mirror of https://github.com/stashapp/stash.git
1205 lines
32 KiB
Go
1205 lines
32 KiB
Go
package manager
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/stashapp/stash/internal/manager/config"
|
|
"github.com/stashapp/stash/pkg/file"
|
|
"github.com/stashapp/stash/pkg/fsutil"
|
|
"github.com/stashapp/stash/pkg/gallery"
|
|
"github.com/stashapp/stash/pkg/image"
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
"github.com/stashapp/stash/pkg/models"
|
|
"github.com/stashapp/stash/pkg/models/json"
|
|
"github.com/stashapp/stash/pkg/models/jsonschema"
|
|
"github.com/stashapp/stash/pkg/models/paths"
|
|
"github.com/stashapp/stash/pkg/movie"
|
|
"github.com/stashapp/stash/pkg/performer"
|
|
"github.com/stashapp/stash/pkg/scene"
|
|
"github.com/stashapp/stash/pkg/sliceutil/intslice"
|
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
|
"github.com/stashapp/stash/pkg/studio"
|
|
"github.com/stashapp/stash/pkg/tag"
|
|
"github.com/stashapp/stash/pkg/utils"
|
|
)
|
|
|
|
type ExportTask struct {
|
|
txnManager Repository
|
|
full bool
|
|
|
|
baseDir string
|
|
json jsonUtils
|
|
|
|
fileNamingAlgorithm models.HashAlgorithm
|
|
|
|
scenes *exportSpec
|
|
images *exportSpec
|
|
performers *exportSpec
|
|
movies *exportSpec
|
|
tags *exportSpec
|
|
studios *exportSpec
|
|
galleries *exportSpec
|
|
|
|
includeDependencies bool
|
|
|
|
DownloadHash string
|
|
}
|
|
|
|
type ExportObjectTypeInput struct {
|
|
Ids []string `json:"ids"`
|
|
All *bool `json:"all"`
|
|
}
|
|
|
|
type ExportObjectsInput struct {
|
|
Scenes *ExportObjectTypeInput `json:"scenes"`
|
|
Images *ExportObjectTypeInput `json:"images"`
|
|
Studios *ExportObjectTypeInput `json:"studios"`
|
|
Performers *ExportObjectTypeInput `json:"performers"`
|
|
Tags *ExportObjectTypeInput `json:"tags"`
|
|
Movies *ExportObjectTypeInput `json:"movies"`
|
|
Galleries *ExportObjectTypeInput `json:"galleries"`
|
|
IncludeDependencies *bool `json:"includeDependencies"`
|
|
}
|
|
|
|
type exportSpec struct {
|
|
IDs []int
|
|
all bool
|
|
}
|
|
|
|
func newExportSpec(input *ExportObjectTypeInput) *exportSpec {
|
|
if input == nil {
|
|
return &exportSpec{}
|
|
}
|
|
|
|
ids, _ := stringslice.StringSliceToIntSlice(input.Ids)
|
|
|
|
ret := &exportSpec{
|
|
IDs: ids,
|
|
}
|
|
|
|
if input.All != nil {
|
|
ret.all = *input.All
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func CreateExportTask(a models.HashAlgorithm, input ExportObjectsInput) *ExportTask {
|
|
includeDeps := false
|
|
if input.IncludeDependencies != nil {
|
|
includeDeps = *input.IncludeDependencies
|
|
}
|
|
|
|
return &ExportTask{
|
|
txnManager: GetInstance().Repository,
|
|
fileNamingAlgorithm: a,
|
|
scenes: newExportSpec(input.Scenes),
|
|
images: newExportSpec(input.Images),
|
|
performers: newExportSpec(input.Performers),
|
|
movies: newExportSpec(input.Movies),
|
|
tags: newExportSpec(input.Tags),
|
|
studios: newExportSpec(input.Studios),
|
|
galleries: newExportSpec(input.Galleries),
|
|
includeDependencies: includeDeps,
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) Start(ctx context.Context, wg *sync.WaitGroup) {
|
|
defer wg.Done()
|
|
// @manager.total = Scene.count + Gallery.count + Performer.count + Studio.count + Movie.count
|
|
workerCount := runtime.GOMAXPROCS(0) // set worker count to number of cpus available
|
|
|
|
startTime := time.Now()
|
|
|
|
if t.full {
|
|
t.baseDir = config.GetInstance().GetMetadataPath()
|
|
} else {
|
|
var err error
|
|
t.baseDir, err = instance.Paths.Generated.TempDir("export")
|
|
if err != nil {
|
|
logger.Errorf("error creating temporary directory for export: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
defer func() {
|
|
err := fsutil.RemoveDir(t.baseDir)
|
|
if err != nil {
|
|
logger.Errorf("error removing directory %s: %s", t.baseDir, err.Error())
|
|
}
|
|
}()
|
|
}
|
|
|
|
if t.baseDir == "" {
|
|
logger.Errorf("baseDir must not be empty")
|
|
return
|
|
}
|
|
|
|
t.json = jsonUtils{
|
|
json: *paths.GetJSONPaths(t.baseDir),
|
|
}
|
|
|
|
paths.EmptyJSONDirs(t.baseDir)
|
|
paths.EnsureJSONDirs(t.baseDir)
|
|
|
|
txnErr := t.txnManager.WithTxn(ctx, func(ctx context.Context) error {
|
|
r := t.txnManager
|
|
|
|
// include movie scenes and gallery images
|
|
if !t.full {
|
|
// only include movie scenes if includeDependencies is also set
|
|
if !t.scenes.all && t.includeDependencies {
|
|
t.populateMovieScenes(ctx, r)
|
|
}
|
|
|
|
// always export gallery images
|
|
if !t.images.all {
|
|
t.populateGalleryImages(ctx, r)
|
|
}
|
|
}
|
|
|
|
t.ExportScenes(ctx, workerCount, r)
|
|
t.ExportImages(ctx, workerCount, r)
|
|
t.ExportGalleries(ctx, workerCount, r)
|
|
t.ExportMovies(ctx, workerCount, r)
|
|
t.ExportPerformers(ctx, workerCount, r)
|
|
t.ExportStudios(ctx, workerCount, r)
|
|
t.ExportTags(ctx, workerCount, r)
|
|
|
|
if t.full {
|
|
t.ExportScrapedItems(ctx, r)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if txnErr != nil {
|
|
logger.Warnf("error while running export transaction: %v", txnErr)
|
|
}
|
|
|
|
if !t.full {
|
|
err := t.generateDownload()
|
|
if err != nil {
|
|
logger.Errorf("error generating download link: %s", err.Error())
|
|
return
|
|
}
|
|
}
|
|
logger.Infof("Export complete in %s.", time.Since(startTime))
|
|
}
|
|
|
|
func (t *ExportTask) generateDownload() error {
|
|
// zip the files and register a download link
|
|
if err := fsutil.EnsureDir(instance.Paths.Generated.Downloads); err != nil {
|
|
return err
|
|
}
|
|
z, err := os.CreateTemp(instance.Paths.Generated.Downloads, "export*.zip")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer z.Close()
|
|
|
|
err = t.zipFiles(z)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
t.DownloadHash, err = instance.DownloadStore.RegisterFile(z.Name(), "", false)
|
|
if err != nil {
|
|
return fmt.Errorf("error registering file for download: %w", err)
|
|
}
|
|
logger.Debugf("Generated zip file %s with hash %s", z.Name(), t.DownloadHash)
|
|
return nil
|
|
}
|
|
|
|
func (t *ExportTask) zipFiles(w io.Writer) error {
|
|
z := zip.NewWriter(w)
|
|
defer z.Close()
|
|
|
|
u := jsonUtils{
|
|
json: *paths.GetJSONPaths(""),
|
|
}
|
|
|
|
walkWarn(t.json.json.Tags, t.zipWalkFunc(u.json.Tags, z))
|
|
walkWarn(t.json.json.Galleries, t.zipWalkFunc(u.json.Galleries, z))
|
|
walkWarn(t.json.json.Performers, t.zipWalkFunc(u.json.Performers, z))
|
|
walkWarn(t.json.json.Studios, t.zipWalkFunc(u.json.Studios, z))
|
|
walkWarn(t.json.json.Movies, t.zipWalkFunc(u.json.Movies, z))
|
|
walkWarn(t.json.json.Scenes, t.zipWalkFunc(u.json.Scenes, z))
|
|
walkWarn(t.json.json.Images, t.zipWalkFunc(u.json.Images, z))
|
|
|
|
return nil
|
|
}
|
|
|
|
// like filepath.Walk but issue a warning on error
|
|
func walkWarn(root string, fn filepath.WalkFunc) {
|
|
if err := filepath.Walk(root, fn); err != nil {
|
|
logger.Warnf("error walking structure %v: %v", root, err)
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) zipWalkFunc(outDir string, z *zip.Writer) filepath.WalkFunc {
|
|
return func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
return t.zipFile(path, outDir, z)
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) zipFile(fn, outDir string, z *zip.Writer) error {
|
|
bn := filepath.Base(fn)
|
|
|
|
p := filepath.Join(outDir, bn)
|
|
p = filepath.ToSlash(p)
|
|
|
|
f, err := z.Create(p)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating zip entry for %s: %s", fn, err.Error())
|
|
}
|
|
|
|
i, err := os.Open(fn)
|
|
if err != nil {
|
|
return fmt.Errorf("error opening %s: %s", fn, err.Error())
|
|
}
|
|
|
|
defer i.Close()
|
|
|
|
if _, err := io.Copy(f, i); err != nil {
|
|
return fmt.Errorf("error writing %s to zip: %s", fn, err.Error())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *ExportTask) populateMovieScenes(ctx context.Context, repo Repository) {
|
|
reader := repo.Movie
|
|
sceneReader := repo.Scene
|
|
|
|
var movies []*models.Movie
|
|
var err error
|
|
all := t.full || (t.movies != nil && t.movies.all)
|
|
if all {
|
|
movies, err = reader.All(ctx)
|
|
} else if t.movies != nil && len(t.movies.IDs) > 0 {
|
|
movies, err = reader.FindMany(ctx, t.movies.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[movies] failed to fetch movies: %s", err.Error())
|
|
}
|
|
|
|
for _, m := range movies {
|
|
scenes, err := sceneReader.FindByMovieID(ctx, m.ID)
|
|
if err != nil {
|
|
logger.Errorf("[movies] <%s> failed to fetch scenes for movie: %s", m.Checksum, err.Error())
|
|
continue
|
|
}
|
|
|
|
for _, s := range scenes {
|
|
t.scenes.IDs = intslice.IntAppendUnique(t.scenes.IDs, s.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) populateGalleryImages(ctx context.Context, repo Repository) {
|
|
reader := repo.Gallery
|
|
imageReader := repo.Image
|
|
|
|
var galleries []*models.Gallery
|
|
var err error
|
|
all := t.full || (t.galleries != nil && t.galleries.all)
|
|
if all {
|
|
galleries, err = reader.All(ctx)
|
|
} else if t.galleries != nil && len(t.galleries.IDs) > 0 {
|
|
galleries, err = reader.FindMany(ctx, t.galleries.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[galleries] failed to fetch galleries: %s", err.Error())
|
|
}
|
|
|
|
for _, g := range galleries {
|
|
if err := g.LoadFiles(ctx, reader); err != nil {
|
|
logger.Errorf("[galleries] <%s> failed to fetch files for gallery: %s", g.DisplayName(), err.Error())
|
|
continue
|
|
}
|
|
|
|
images, err := imageReader.FindByGalleryID(ctx, g.ID)
|
|
if err != nil {
|
|
logger.Errorf("[galleries] <%s> failed to fetch images for gallery: %s", g.PrimaryChecksum(), err.Error())
|
|
continue
|
|
}
|
|
|
|
for _, i := range images {
|
|
t.images.IDs = intslice.IntAppendUnique(t.images.IDs, i.ID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) ExportScenes(ctx context.Context, workers int, repo Repository) {
|
|
var scenesWg sync.WaitGroup
|
|
|
|
sceneReader := repo.Scene
|
|
|
|
var scenes []*models.Scene
|
|
var err error
|
|
all := t.full || (t.scenes != nil && t.scenes.all)
|
|
if all {
|
|
scenes, err = sceneReader.All(ctx)
|
|
} else if t.scenes != nil && len(t.scenes.IDs) > 0 {
|
|
scenes, err = sceneReader.FindMany(ctx, t.scenes.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[scenes] failed to fetch scenes: %s", err.Error())
|
|
}
|
|
|
|
jobCh := make(chan *models.Scene, workers*2) // make a buffered channel to feed workers
|
|
|
|
logger.Info("[scenes] exporting")
|
|
startTime := time.Now()
|
|
|
|
for w := 0; w < workers; w++ { // create export Scene workers
|
|
scenesWg.Add(1)
|
|
go exportScene(ctx, &scenesWg, jobCh, repo, t)
|
|
}
|
|
|
|
for i, scene := range scenes {
|
|
index := i + 1
|
|
|
|
if (i % 100) == 0 { // make progress easier to read
|
|
logger.Progressf("[scenes] %d of %d", index, len(scenes))
|
|
}
|
|
jobCh <- scene // feed workers
|
|
}
|
|
|
|
close(jobCh) // close channel so that workers will know no more jobs are available
|
|
scenesWg.Wait()
|
|
|
|
logger.Infof("[scenes] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
|
}
|
|
|
|
func exportFile(f file.File, t *ExportTask) {
|
|
newFileJSON := fileToJSON(f)
|
|
|
|
fn := newFileJSON.Filename()
|
|
|
|
if err := t.json.saveFile(fn, newFileJSON); err != nil {
|
|
logger.Errorf("[files] <%s> failed to save json: %s", fn, err.Error())
|
|
}
|
|
}
|
|
|
|
func fileToJSON(f file.File) jsonschema.DirEntry {
|
|
bf := f.Base()
|
|
|
|
base := jsonschema.BaseFile{
|
|
BaseDirEntry: jsonschema.BaseDirEntry{
|
|
Type: jsonschema.DirEntryTypeFile,
|
|
ModTime: json.JSONTime{Time: bf.ModTime},
|
|
Path: bf.Path,
|
|
CreatedAt: json.JSONTime{Time: bf.CreatedAt},
|
|
UpdatedAt: json.JSONTime{Time: bf.UpdatedAt},
|
|
},
|
|
Size: bf.Size,
|
|
}
|
|
|
|
if bf.ZipFile != nil {
|
|
base.ZipFile = bf.ZipFile.Base().Path
|
|
}
|
|
|
|
for _, fp := range bf.Fingerprints {
|
|
base.Fingerprints = append(base.Fingerprints, jsonschema.Fingerprint{
|
|
Type: fp.Type,
|
|
Fingerprint: fp.Fingerprint,
|
|
})
|
|
}
|
|
|
|
switch ff := f.(type) {
|
|
case *file.VideoFile:
|
|
base.Type = jsonschema.DirEntryTypeVideo
|
|
return jsonschema.VideoFile{
|
|
BaseFile: &base,
|
|
Format: ff.Format,
|
|
Width: ff.Width,
|
|
Height: ff.Height,
|
|
Duration: ff.Duration,
|
|
VideoCodec: ff.VideoCodec,
|
|
AudioCodec: ff.AudioCodec,
|
|
FrameRate: ff.FrameRate,
|
|
BitRate: ff.BitRate,
|
|
Interactive: ff.Interactive,
|
|
InteractiveSpeed: ff.InteractiveSpeed,
|
|
}
|
|
case *file.ImageFile:
|
|
base.Type = jsonschema.DirEntryTypeImage
|
|
return jsonschema.ImageFile{
|
|
BaseFile: &base,
|
|
Format: ff.Format,
|
|
Width: ff.Width,
|
|
Height: ff.Height,
|
|
}
|
|
}
|
|
|
|
return &base
|
|
}
|
|
|
|
func exportFolder(f file.Folder, t *ExportTask) {
|
|
newFileJSON := folderToJSON(f)
|
|
|
|
fn := newFileJSON.Filename()
|
|
|
|
if err := t.json.saveFile(fn, newFileJSON); err != nil {
|
|
logger.Errorf("[files] <%s> failed to save json: %s", fn, err.Error())
|
|
}
|
|
}
|
|
|
|
func folderToJSON(f file.Folder) jsonschema.DirEntry {
|
|
base := jsonschema.BaseDirEntry{
|
|
Type: jsonschema.DirEntryTypeFolder,
|
|
ModTime: json.JSONTime{Time: f.ModTime},
|
|
Path: f.Path,
|
|
CreatedAt: json.JSONTime{Time: f.CreatedAt},
|
|
UpdatedAt: json.JSONTime{Time: f.UpdatedAt},
|
|
}
|
|
|
|
if f.ZipFile != nil {
|
|
base.ZipFile = f.ZipFile.Base().Path
|
|
}
|
|
|
|
return &base
|
|
}
|
|
|
|
func exportScene(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Scene, repo Repository, t *ExportTask) {
|
|
defer wg.Done()
|
|
sceneReader := repo.Scene
|
|
studioReader := repo.Studio
|
|
movieReader := repo.Movie
|
|
galleryReader := repo.Gallery
|
|
performerReader := repo.Performer
|
|
tagReader := repo.Tag
|
|
sceneMarkerReader := repo.SceneMarker
|
|
|
|
for s := range jobChan {
|
|
sceneHash := s.GetHash(t.fileNamingAlgorithm)
|
|
|
|
if err := s.LoadRelationships(ctx, sceneReader); err != nil {
|
|
logger.Errorf("[scenes] <%s> error loading scene relationships: %v", sceneHash, err)
|
|
}
|
|
|
|
newSceneJSON, err := scene.ToBasicJSON(ctx, sceneReader, s)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene JSON: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
// export files
|
|
for _, f := range s.Files.List() {
|
|
exportFile(f, t)
|
|
}
|
|
|
|
newSceneJSON.Studio, err = scene.GetStudioName(ctx, studioReader, s)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene studio name: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
galleries, err := galleryReader.FindBySceneID(ctx, s.ID)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene gallery checksums: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
for _, g := range galleries {
|
|
if err := g.LoadFiles(ctx, galleryReader); err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene gallery files: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
}
|
|
|
|
newSceneJSON.Galleries = gallery.GetRefs(galleries)
|
|
|
|
newSceneJSON.ResumeTime = s.ResumeTime
|
|
newSceneJSON.PlayCount = s.PlayCount
|
|
newSceneJSON.PlayDuration = s.PlayDuration
|
|
|
|
performers, err := performerReader.FindBySceneID(ctx, s.ID)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene performer names: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newSceneJSON.Performers = performer.GetNames(performers)
|
|
|
|
newSceneJSON.Tags, err = scene.GetTagNames(ctx, tagReader, s)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene tag names: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newSceneJSON.Markers, err = scene.GetSceneMarkersJSON(ctx, sceneMarkerReader, tagReader, s)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene markers JSON: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newSceneJSON.Movies, err = scene.GetSceneMoviesJSON(ctx, movieReader, s)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene movies JSON: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
if t.includeDependencies {
|
|
if s.StudioID != nil {
|
|
t.studios.IDs = intslice.IntAppendUnique(t.studios.IDs, *s.StudioID)
|
|
}
|
|
|
|
t.galleries.IDs = intslice.IntAppendUniques(t.galleries.IDs, gallery.GetIDs(galleries))
|
|
|
|
tagIDs, err := scene.GetDependentTagIDs(ctx, tagReader, sceneMarkerReader, s)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene tags: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
t.tags.IDs = intslice.IntAppendUniques(t.tags.IDs, tagIDs)
|
|
|
|
movieIDs, err := scene.GetDependentMovieIDs(ctx, s)
|
|
if err != nil {
|
|
logger.Errorf("[scenes] <%s> error getting scene movies: %s", sceneHash, err.Error())
|
|
continue
|
|
}
|
|
t.movies.IDs = intslice.IntAppendUniques(t.movies.IDs, movieIDs)
|
|
|
|
t.performers.IDs = intslice.IntAppendUniques(t.performers.IDs, performer.GetIDs(performers))
|
|
}
|
|
|
|
basename := filepath.Base(s.Path)
|
|
hash := s.OSHash
|
|
|
|
fn := newSceneJSON.Filename(s.ID, basename, hash)
|
|
|
|
if err := t.json.saveScene(fn, newSceneJSON); err != nil {
|
|
logger.Errorf("[scenes] <%s> failed to save json: %s", sceneHash, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) ExportImages(ctx context.Context, workers int, repo Repository) {
|
|
var imagesWg sync.WaitGroup
|
|
|
|
imageReader := repo.Image
|
|
|
|
var images []*models.Image
|
|
var err error
|
|
all := t.full || (t.images != nil && t.images.all)
|
|
if all {
|
|
images, err = imageReader.All(ctx)
|
|
} else if t.images != nil && len(t.images.IDs) > 0 {
|
|
images, err = imageReader.FindMany(ctx, t.images.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[images] failed to fetch images: %s", err.Error())
|
|
}
|
|
|
|
jobCh := make(chan *models.Image, workers*2) // make a buffered channel to feed workers
|
|
|
|
logger.Info("[images] exporting")
|
|
startTime := time.Now()
|
|
|
|
for w := 0; w < workers; w++ { // create export Image workers
|
|
imagesWg.Add(1)
|
|
go exportImage(ctx, &imagesWg, jobCh, repo, t)
|
|
}
|
|
|
|
for i, image := range images {
|
|
index := i + 1
|
|
|
|
if (i % 100) == 0 { // make progress easier to read
|
|
logger.Progressf("[images] %d of %d", index, len(images))
|
|
}
|
|
jobCh <- image // feed workers
|
|
}
|
|
|
|
close(jobCh) // close channel so that workers will know no more jobs are available
|
|
imagesWg.Wait()
|
|
|
|
logger.Infof("[images] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
|
}
|
|
|
|
func exportImage(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Image, repo Repository, t *ExportTask) {
|
|
defer wg.Done()
|
|
studioReader := repo.Studio
|
|
galleryReader := repo.Gallery
|
|
performerReader := repo.Performer
|
|
tagReader := repo.Tag
|
|
|
|
for s := range jobChan {
|
|
imageHash := s.Checksum
|
|
|
|
if err := s.LoadFiles(ctx, repo.Image); err != nil {
|
|
logger.Errorf("[images] <%s> error getting image files: %s", imageHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newImageJSON := image.ToBasicJSON(s)
|
|
|
|
// export files
|
|
for _, f := range s.Files.List() {
|
|
exportFile(f, t)
|
|
}
|
|
|
|
var err error
|
|
newImageJSON.Studio, err = image.GetStudioName(ctx, studioReader, s)
|
|
if err != nil {
|
|
logger.Errorf("[images] <%s> error getting image studio name: %s", imageHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
imageGalleries, err := galleryReader.FindByImageID(ctx, s.ID)
|
|
if err != nil {
|
|
logger.Errorf("[images] <%s> error getting image galleries: %s", imageHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
for _, g := range imageGalleries {
|
|
if err := g.LoadFiles(ctx, galleryReader); err != nil {
|
|
logger.Errorf("[images] <%s> error getting image gallery files: %s", imageHash, err.Error())
|
|
continue
|
|
}
|
|
}
|
|
|
|
newImageJSON.Galleries = gallery.GetRefs(imageGalleries)
|
|
|
|
performers, err := performerReader.FindByImageID(ctx, s.ID)
|
|
if err != nil {
|
|
logger.Errorf("[images] <%s> error getting image performer names: %s", imageHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newImageJSON.Performers = performer.GetNames(performers)
|
|
|
|
tags, err := tagReader.FindByImageID(ctx, s.ID)
|
|
if err != nil {
|
|
logger.Errorf("[images] <%s> error getting image tag names: %s", imageHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newImageJSON.Tags = tag.GetNames(tags)
|
|
|
|
if t.includeDependencies {
|
|
if s.StudioID != nil {
|
|
t.studios.IDs = intslice.IntAppendUnique(t.studios.IDs, *s.StudioID)
|
|
}
|
|
|
|
t.galleries.IDs = intslice.IntAppendUniques(t.galleries.IDs, gallery.GetIDs(imageGalleries))
|
|
t.tags.IDs = intslice.IntAppendUniques(t.tags.IDs, tag.GetIDs(tags))
|
|
t.performers.IDs = intslice.IntAppendUniques(t.performers.IDs, performer.GetIDs(performers))
|
|
}
|
|
|
|
fn := newImageJSON.Filename(filepath.Base(s.Path), s.Checksum)
|
|
|
|
if err := t.json.saveImage(fn, newImageJSON); err != nil {
|
|
logger.Errorf("[images] <%s> failed to save json: %s", imageHash, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) ExportGalleries(ctx context.Context, workers int, repo Repository) {
|
|
var galleriesWg sync.WaitGroup
|
|
|
|
reader := repo.Gallery
|
|
|
|
var galleries []*models.Gallery
|
|
var err error
|
|
all := t.full || (t.galleries != nil && t.galleries.all)
|
|
if all {
|
|
galleries, err = reader.All(ctx)
|
|
} else if t.galleries != nil && len(t.galleries.IDs) > 0 {
|
|
galleries, err = reader.FindMany(ctx, t.galleries.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[galleries] failed to fetch galleries: %s", err.Error())
|
|
}
|
|
|
|
jobCh := make(chan *models.Gallery, workers*2) // make a buffered channel to feed workers
|
|
|
|
logger.Info("[galleries] exporting")
|
|
startTime := time.Now()
|
|
|
|
for w := 0; w < workers; w++ { // create export Scene workers
|
|
galleriesWg.Add(1)
|
|
go exportGallery(ctx, &galleriesWg, jobCh, repo, t)
|
|
}
|
|
|
|
for i, gallery := range galleries {
|
|
index := i + 1
|
|
|
|
if (i % 100) == 0 { // make progress easier to read
|
|
logger.Progressf("[galleries] %d of %d", index, len(galleries))
|
|
}
|
|
|
|
jobCh <- gallery
|
|
}
|
|
|
|
close(jobCh) // close channel so that workers will know no more jobs are available
|
|
galleriesWg.Wait()
|
|
|
|
logger.Infof("[galleries] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
|
}
|
|
|
|
func exportGallery(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Gallery, repo Repository, t *ExportTask) {
|
|
defer wg.Done()
|
|
studioReader := repo.Studio
|
|
performerReader := repo.Performer
|
|
tagReader := repo.Tag
|
|
galleryChapterReader := repo.GalleryChapter
|
|
|
|
for g := range jobChan {
|
|
if err := g.LoadFiles(ctx, repo.Gallery); err != nil {
|
|
logger.Errorf("[galleries] <%s> failed to fetch files for gallery: %s", g.DisplayName(), err.Error())
|
|
continue
|
|
}
|
|
|
|
galleryHash := g.PrimaryChecksum()
|
|
|
|
newGalleryJSON, err := gallery.ToBasicJSON(g)
|
|
if err != nil {
|
|
logger.Errorf("[galleries] <%s> error getting gallery JSON: %s", galleryHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
// export files
|
|
for _, f := range g.Files.List() {
|
|
exportFile(f, t)
|
|
}
|
|
|
|
// export folder if necessary
|
|
if g.FolderID != nil {
|
|
folder, err := repo.Folder.Find(ctx, *g.FolderID)
|
|
if err != nil {
|
|
logger.Errorf("[galleries] <%s> error getting gallery folder: %v", galleryHash, err)
|
|
continue
|
|
}
|
|
|
|
if folder == nil {
|
|
logger.Errorf("[galleries] <%s> unable to find gallery folder", galleryHash)
|
|
continue
|
|
}
|
|
|
|
exportFolder(*folder, t)
|
|
}
|
|
|
|
newGalleryJSON.Studio, err = gallery.GetStudioName(ctx, studioReader, g)
|
|
if err != nil {
|
|
logger.Errorf("[galleries] <%s> error getting gallery studio name: %s", galleryHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
performers, err := performerReader.FindByGalleryID(ctx, g.ID)
|
|
if err != nil {
|
|
logger.Errorf("[galleries] <%s> error getting gallery performer names: %s", galleryHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newGalleryJSON.Performers = performer.GetNames(performers)
|
|
|
|
tags, err := tagReader.FindByGalleryID(ctx, g.ID)
|
|
if err != nil {
|
|
logger.Errorf("[galleries] <%s> error getting gallery tag names: %s", galleryHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newGalleryJSON.Chapters, err = gallery.GetGalleryChaptersJSON(ctx, galleryChapterReader, g)
|
|
if err != nil {
|
|
logger.Errorf("[galleries] <%s> error getting gallery chapters JSON: %s", galleryHash, err.Error())
|
|
continue
|
|
}
|
|
|
|
newGalleryJSON.Tags = tag.GetNames(tags)
|
|
|
|
if t.includeDependencies {
|
|
if g.StudioID != nil {
|
|
t.studios.IDs = intslice.IntAppendUnique(t.studios.IDs, *g.StudioID)
|
|
}
|
|
|
|
t.tags.IDs = intslice.IntAppendUniques(t.tags.IDs, tag.GetIDs(tags))
|
|
t.performers.IDs = intslice.IntAppendUniques(t.performers.IDs, performer.GetIDs(performers))
|
|
}
|
|
|
|
basename := ""
|
|
// use id in case multiple galleries with the same basename
|
|
hash := strconv.Itoa(g.ID)
|
|
|
|
switch {
|
|
case g.Path != "":
|
|
basename = filepath.Base(g.Path)
|
|
default:
|
|
basename = g.Title
|
|
}
|
|
|
|
fn := newGalleryJSON.Filename(basename, hash)
|
|
|
|
if err := t.json.saveGallery(fn, newGalleryJSON); err != nil {
|
|
logger.Errorf("[galleries] <%s> failed to save json: %s", galleryHash, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) ExportPerformers(ctx context.Context, workers int, repo Repository) {
|
|
var performersWg sync.WaitGroup
|
|
|
|
reader := repo.Performer
|
|
var performers []*models.Performer
|
|
var err error
|
|
all := t.full || (t.performers != nil && t.performers.all)
|
|
if all {
|
|
performers, err = reader.All(ctx)
|
|
} else if t.performers != nil && len(t.performers.IDs) > 0 {
|
|
performers, err = reader.FindMany(ctx, t.performers.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[performers] failed to fetch performers: %s", err.Error())
|
|
}
|
|
jobCh := make(chan *models.Performer, workers*2) // make a buffered channel to feed workers
|
|
|
|
logger.Info("[performers] exporting")
|
|
startTime := time.Now()
|
|
|
|
for w := 0; w < workers; w++ { // create export Performer workers
|
|
performersWg.Add(1)
|
|
go t.exportPerformer(ctx, &performersWg, jobCh, repo)
|
|
}
|
|
|
|
for i, performer := range performers {
|
|
index := i + 1
|
|
logger.Progressf("[performers] %d of %d", index, len(performers))
|
|
|
|
jobCh <- performer // feed workers
|
|
}
|
|
|
|
close(jobCh) // close channel so workers will know that no more jobs are available
|
|
performersWg.Wait()
|
|
|
|
logger.Infof("[performers] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
|
}
|
|
|
|
func (t *ExportTask) exportPerformer(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Performer, repo Repository) {
|
|
defer wg.Done()
|
|
|
|
performerReader := repo.Performer
|
|
|
|
for p := range jobChan {
|
|
newPerformerJSON, err := performer.ToJSON(ctx, performerReader, p)
|
|
|
|
if err != nil {
|
|
logger.Errorf("[performers] <%s> error getting performer JSON: %s", p.Name, err.Error())
|
|
continue
|
|
}
|
|
|
|
tags, err := repo.Tag.FindByPerformerID(ctx, p.ID)
|
|
if err != nil {
|
|
logger.Errorf("[performers] <%s> error getting performer tags: %s", p.Name, err.Error())
|
|
continue
|
|
}
|
|
|
|
newPerformerJSON.Tags = tag.GetNames(tags)
|
|
|
|
if t.includeDependencies {
|
|
t.tags.IDs = intslice.IntAppendUniques(t.tags.IDs, tag.GetIDs(tags))
|
|
}
|
|
|
|
fn := newPerformerJSON.Filename()
|
|
|
|
if err := t.json.savePerformer(fn, newPerformerJSON); err != nil {
|
|
logger.Errorf("[performers] <%s> failed to save json: %s", p.Name, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) ExportStudios(ctx context.Context, workers int, repo Repository) {
|
|
var studiosWg sync.WaitGroup
|
|
|
|
reader := repo.Studio
|
|
var studios []*models.Studio
|
|
var err error
|
|
all := t.full || (t.studios != nil && t.studios.all)
|
|
if all {
|
|
studios, err = reader.All(ctx)
|
|
} else if t.studios != nil && len(t.studios.IDs) > 0 {
|
|
studios, err = reader.FindMany(ctx, t.studios.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[studios] failed to fetch studios: %s", err.Error())
|
|
}
|
|
|
|
logger.Info("[studios] exporting")
|
|
startTime := time.Now()
|
|
|
|
jobCh := make(chan *models.Studio, workers*2) // make a buffered channel to feed workers
|
|
|
|
for w := 0; w < workers; w++ { // create export Studio workers
|
|
studiosWg.Add(1)
|
|
go t.exportStudio(ctx, &studiosWg, jobCh, repo)
|
|
}
|
|
|
|
for i, studio := range studios {
|
|
index := i + 1
|
|
logger.Progressf("[studios] %d of %d", index, len(studios))
|
|
|
|
jobCh <- studio // feed workers
|
|
}
|
|
|
|
close(jobCh)
|
|
studiosWg.Wait()
|
|
|
|
logger.Infof("[studios] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
|
}
|
|
|
|
func (t *ExportTask) exportStudio(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Studio, repo Repository) {
|
|
defer wg.Done()
|
|
|
|
studioReader := repo.Studio
|
|
|
|
for s := range jobChan {
|
|
newStudioJSON, err := studio.ToJSON(ctx, studioReader, s)
|
|
|
|
if err != nil {
|
|
logger.Errorf("[studios] <%s> error getting studio JSON: %s", s.Checksum, err.Error())
|
|
continue
|
|
}
|
|
|
|
fn := newStudioJSON.Filename()
|
|
|
|
if err := t.json.saveStudio(fn, newStudioJSON); err != nil {
|
|
logger.Errorf("[studios] <%s> failed to save json: %s", s.Checksum, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) ExportTags(ctx context.Context, workers int, repo Repository) {
|
|
var tagsWg sync.WaitGroup
|
|
|
|
reader := repo.Tag
|
|
var tags []*models.Tag
|
|
var err error
|
|
all := t.full || (t.tags != nil && t.tags.all)
|
|
if all {
|
|
tags, err = reader.All(ctx)
|
|
} else if t.tags != nil && len(t.tags.IDs) > 0 {
|
|
tags, err = reader.FindMany(ctx, t.tags.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[tags] failed to fetch tags: %s", err.Error())
|
|
}
|
|
|
|
logger.Info("[tags] exporting")
|
|
startTime := time.Now()
|
|
|
|
jobCh := make(chan *models.Tag, workers*2) // make a buffered channel to feed workers
|
|
|
|
for w := 0; w < workers; w++ { // create export Tag workers
|
|
tagsWg.Add(1)
|
|
go t.exportTag(ctx, &tagsWg, jobCh, repo)
|
|
}
|
|
|
|
for i, tag := range tags {
|
|
index := i + 1
|
|
logger.Progressf("[tags] %d of %d", index, len(tags))
|
|
|
|
jobCh <- tag // feed workers
|
|
}
|
|
|
|
close(jobCh)
|
|
tagsWg.Wait()
|
|
|
|
logger.Infof("[tags] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
|
}
|
|
|
|
func (t *ExportTask) exportTag(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Tag, repo Repository) {
|
|
defer wg.Done()
|
|
|
|
tagReader := repo.Tag
|
|
|
|
for thisTag := range jobChan {
|
|
newTagJSON, err := tag.ToJSON(ctx, tagReader, thisTag)
|
|
|
|
if err != nil {
|
|
logger.Errorf("[tags] <%s> error getting tag JSON: %s", thisTag.Name, err.Error())
|
|
continue
|
|
}
|
|
|
|
fn := newTagJSON.Filename()
|
|
|
|
if err := t.json.saveTag(fn, newTagJSON); err != nil {
|
|
logger.Errorf("[tags] <%s> failed to save json: %s", fn, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) ExportMovies(ctx context.Context, workers int, repo Repository) {
|
|
var moviesWg sync.WaitGroup
|
|
|
|
reader := repo.Movie
|
|
var movies []*models.Movie
|
|
var err error
|
|
all := t.full || (t.movies != nil && t.movies.all)
|
|
if all {
|
|
movies, err = reader.All(ctx)
|
|
} else if t.movies != nil && len(t.movies.IDs) > 0 {
|
|
movies, err = reader.FindMany(ctx, t.movies.IDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Errorf("[movies] failed to fetch movies: %s", err.Error())
|
|
}
|
|
|
|
logger.Info("[movies] exporting")
|
|
startTime := time.Now()
|
|
|
|
jobCh := make(chan *models.Movie, workers*2) // make a buffered channel to feed workers
|
|
|
|
for w := 0; w < workers; w++ { // create export Studio workers
|
|
moviesWg.Add(1)
|
|
go t.exportMovie(ctx, &moviesWg, jobCh, repo)
|
|
}
|
|
|
|
for i, movie := range movies {
|
|
index := i + 1
|
|
logger.Progressf("[movies] %d of %d", index, len(movies))
|
|
|
|
jobCh <- movie // feed workers
|
|
}
|
|
|
|
close(jobCh)
|
|
moviesWg.Wait()
|
|
|
|
logger.Infof("[movies] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
|
|
|
}
|
|
func (t *ExportTask) exportMovie(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.Movie, repo Repository) {
|
|
defer wg.Done()
|
|
|
|
movieReader := repo.Movie
|
|
studioReader := repo.Studio
|
|
|
|
for m := range jobChan {
|
|
newMovieJSON, err := movie.ToJSON(ctx, movieReader, studioReader, m)
|
|
|
|
if err != nil {
|
|
logger.Errorf("[movies] <%s> error getting tag JSON: %s", m.Checksum, err.Error())
|
|
continue
|
|
}
|
|
|
|
if t.includeDependencies {
|
|
if m.StudioID.Valid {
|
|
t.studios.IDs = intslice.IntAppendUnique(t.studios.IDs, int(m.StudioID.Int64))
|
|
}
|
|
}
|
|
|
|
fn := newMovieJSON.Filename()
|
|
|
|
if err := t.json.saveMovie(fn, newMovieJSON); err != nil {
|
|
logger.Errorf("[movies] <%s> failed to save json: %s", fn, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *ExportTask) ExportScrapedItems(ctx context.Context, repo Repository) {
|
|
qb := repo.ScrapedItem
|
|
sqb := repo.Studio
|
|
scrapedItems, err := qb.All(ctx)
|
|
if err != nil {
|
|
logger.Errorf("[scraped sites] failed to fetch all items: %s", err.Error())
|
|
}
|
|
|
|
logger.Info("[scraped sites] exporting")
|
|
|
|
scraped := []jsonschema.ScrapedItem{}
|
|
|
|
for i, scrapedItem := range scrapedItems {
|
|
index := i + 1
|
|
logger.Progressf("[scraped sites] %d of %d", index, len(scrapedItems))
|
|
|
|
var studioName string
|
|
if scrapedItem.StudioID.Valid {
|
|
studio, _ := sqb.Find(ctx, int(scrapedItem.StudioID.Int64))
|
|
if studio != nil {
|
|
studioName = studio.Name.String
|
|
}
|
|
}
|
|
|
|
newScrapedItemJSON := jsonschema.ScrapedItem{}
|
|
|
|
if scrapedItem.Title.Valid {
|
|
newScrapedItemJSON.Title = scrapedItem.Title.String
|
|
}
|
|
if scrapedItem.Description.Valid {
|
|
newScrapedItemJSON.Description = scrapedItem.Description.String
|
|
}
|
|
if scrapedItem.URL.Valid {
|
|
newScrapedItemJSON.URL = scrapedItem.URL.String
|
|
}
|
|
if scrapedItem.Date.Valid {
|
|
newScrapedItemJSON.Date = utils.GetYMDFromDatabaseDate(scrapedItem.Date.String)
|
|
}
|
|
if scrapedItem.Rating.Valid {
|
|
newScrapedItemJSON.Rating = scrapedItem.Rating.String
|
|
}
|
|
if scrapedItem.Tags.Valid {
|
|
newScrapedItemJSON.Tags = scrapedItem.Tags.String
|
|
}
|
|
if scrapedItem.Models.Valid {
|
|
newScrapedItemJSON.Models = scrapedItem.Models.String
|
|
}
|
|
if scrapedItem.Episode.Valid {
|
|
newScrapedItemJSON.Episode = int(scrapedItem.Episode.Int64)
|
|
}
|
|
if scrapedItem.GalleryFilename.Valid {
|
|
newScrapedItemJSON.GalleryFilename = scrapedItem.GalleryFilename.String
|
|
}
|
|
if scrapedItem.GalleryURL.Valid {
|
|
newScrapedItemJSON.GalleryURL = scrapedItem.GalleryURL.String
|
|
}
|
|
if scrapedItem.VideoFilename.Valid {
|
|
newScrapedItemJSON.VideoFilename = scrapedItem.VideoFilename.String
|
|
}
|
|
if scrapedItem.VideoURL.Valid {
|
|
newScrapedItemJSON.VideoURL = scrapedItem.VideoURL.String
|
|
}
|
|
|
|
newScrapedItemJSON.Studio = studioName
|
|
updatedAt := json.JSONTime{Time: scrapedItem.UpdatedAt.Timestamp} // TODO keeping ruby format
|
|
newScrapedItemJSON.UpdatedAt = updatedAt
|
|
|
|
scraped = append(scraped, newScrapedItemJSON)
|
|
}
|
|
|
|
scrapedJSON, err := t.json.getScraped()
|
|
if err != nil {
|
|
logger.Debugf("[scraped sites] error reading json: %s", err.Error())
|
|
}
|
|
if !jsonschema.CompareJSON(scrapedJSON, scraped) {
|
|
if err := t.json.saveScaped(scraped); err != nil {
|
|
logger.Errorf("[scraped sites] failed to save json: %s", err.Error())
|
|
}
|
|
}
|
|
|
|
logger.Infof("[scraped sites] export complete")
|
|
}
|