Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add selection and export for all list pages #873

Merged
merged 13 commits into from
Oct 30, 2020
9 changes: 9 additions & 0 deletions pkg/gallery/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,12 @@ func GetStudioName(reader models.StudioReader, gallery *models.Gallery) (string,

return "", nil
}

func GetIDs(galleries []*models.Gallery) []int {
var results []int
for _, gallery := range galleries {
results = append(results, gallery.ID)
}

return results
}
86 changes: 81 additions & 5 deletions pkg/manager/task_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,25 @@ func (t *ExportTask) Start(wg *sync.WaitGroup) {

paths.EnsureJSONDirs(t.baseDir)

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

// always export gallery images
if !t.images.all {
t.populateGalleryImages()
}
}

t.ExportScenes(workerCount)
t.ExportImages(workerCount)
t.ExportGalleries(workerCount)
t.ExportMovies(workerCount)
t.ExportPerformers(workerCount)
t.ExportStudios(workerCount)
t.ExportMovies(workerCount)
t.ExportTags(workerCount)

if err := t.json.saveMappings(t.Mappings); err != nil {
Expand Down Expand Up @@ -229,6 +242,66 @@ func (t *ExportTask) zipFile(fn, outDir string, z *zip.Writer) error {
return nil
}

func (t *ExportTask) populateMovieScenes() {
reader := models.NewMovieReaderWriter(nil)
sceneReader := models.NewSceneReaderWriter(nil)

var movies []*models.Movie
var err error
all := t.full || (t.movies != nil && t.movies.all)
if all {
movies, err = reader.All()
} else if t.movies != nil && len(t.movies.IDs) > 0 {
movies, err = reader.FindMany(t.movies.IDs)
}

if err != nil {
logger.Errorf("[movies] failed to fetch movies: %s", err.Error())
}

for _, m := range movies {
scenes, err := sceneReader.FindByMovieID(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 = utils.IntAppendUnique(t.scenes.IDs, s.ID)
}
}
}

func (t *ExportTask) populateGalleryImages() {
reader := models.NewGalleryReaderWriter(nil)
imageReader := models.NewImageReaderWriter(nil)

var galleries []*models.Gallery
var err error
all := t.full || (t.galleries != nil && t.galleries.all)
if all {
galleries, err = reader.All()
} else if t.galleries != nil && len(t.galleries.IDs) > 0 {
galleries, err = reader.FindMany(t.galleries.IDs)
}

if err != nil {
logger.Errorf("[galleries] failed to fetch galleries: %s", err.Error())
}

for _, g := range galleries {
images, err := imageReader.FindByGalleryID(g.ID)
if err != nil {
logger.Errorf("[galleries] <%s> failed to fetch images for gallery: %s", g.Checksum, err.Error())
continue
}

for _, i := range images {
t.images.IDs = utils.IntAppendUnique(t.images.IDs, i.ID)
}
}
}

func (t *ExportTask) ExportScenes(workers int) {
var scenesWg sync.WaitGroup

Expand Down Expand Up @@ -464,10 +537,7 @@ func exportImage(wg *sync.WaitGroup, jobChan <-chan *models.Image, t *ExportTask
t.studios.IDs = utils.IntAppendUnique(t.studios.IDs, int(s.StudioID.Int64))
}

// if imageGallery != nil {
// t.galleries.IDs = utils.IntAppendUnique(t.galleries.IDs, imageGallery.ID)
// }

t.galleries.IDs = utils.IntAppendUniques(t.galleries.IDs, gallery.GetIDs(imageGalleries))
t.tags.IDs = utils.IntAppendUniques(t.tags.IDs, tag.GetIDs(tags))
t.performers.IDs = utils.IntAppendUniques(t.performers.IDs, performer.GetIDs(performers))
}
Expand Down Expand Up @@ -853,6 +923,12 @@ func (t *ExportTask) exportMovie(wg *sync.WaitGroup, jobChan <-chan *models.Movi
continue
}

if t.includeDependencies {
if m.StudioID.Valid {
t.studios.IDs = utils.IntAppendUnique(t.studios.IDs, int(m.StudioID.Int64))
}
}

movieJSON, err := t.json.getMovie(m.Checksum)
if err != nil {
logger.Debugf("[movies] error reading movie json: %s", err.Error())
Expand Down
5 changes: 5 additions & 0 deletions pkg/models/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type ImageReader interface {
// Find(id int) (*Image, error)
FindMany(ids []int) ([]*Image, error)
FindByChecksum(checksum string) (*Image, error)
FindByGalleryID(galleryID int) ([]*Image, error)
// FindByPath(path string) (*Image, error)
// FindByPerformerID(performerID int) ([]*Image, error)
// CountByPerformerID(performerID int) (int, error)
Expand Down Expand Up @@ -55,6 +56,10 @@ func (t *imageReaderWriter) FindByChecksum(checksum string) (*Image, error) {
return t.qb.FindByChecksum(checksum)
}

func (t *imageReaderWriter) FindByGalleryID(galleryID int) ([]*Image, error) {
return t.qb.FindByGalleryID(galleryID)
}

func (t *imageReaderWriter) All() ([]*Image, error) {
return t.qb.All()
}
Expand Down
23 changes: 23 additions & 0 deletions pkg/models/mocks/ImageReaderWriter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions pkg/models/mocks/SceneReaderWriter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion pkg/models/scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type SceneReader interface {
// FindByPerformerID(performerID int) ([]*Scene, error)
// CountByPerformerID(performerID int) (int, error)
// FindByStudioID(studioID int) ([]*Scene, error)
// FindByMovieID(movieID int) ([]*Scene, error)
FindByMovieID(movieID int) ([]*Scene, error)
// CountByMovieID(movieID int) (int, error)
// Count() (int, error)
// SizeCount() (string, error)
Expand Down Expand Up @@ -73,6 +73,10 @@ func (t *sceneReaderWriter) FindByOSHash(oshash string) (*Scene, error) {
return t.qb.FindByOSHash(oshash)
}

func (t *sceneReaderWriter) FindByMovieID(movieID int) ([]*Scene, error) {
return t.qb.FindByMovieID(movieID)
}

func (t *sceneReaderWriter) All() ([]*Scene, error) {
return t.qb.All()
}
Expand Down
2 changes: 1 addition & 1 deletion ui/v2.5/src/components/Changelog/versions/v040.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
### ✨ New Features
* Add selective export of all objects.
* Add stash-box tagger to scenes page.
* Add filters tab in scene page.
* Add selectable streaming quality profiles in the scene player.
* Add scrapers list setting page.
* Add support for individual images and manual creation of galleries.
* Add various fields to galleries.
* Add partial import from zip file.
* Add selective scene export.

### 🎨 Improvements
* Add support for query URL parameter regex replacement when scraping by query URL.
Expand Down
110 changes: 34 additions & 76 deletions ui/v2.5/src/components/Galleries/GalleryCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Card, Button, ButtonGroup, Form } from "react-bootstrap";
import { Button, ButtonGroup } from "react-bootstrap";
import React from "react";
import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { FormattedPlural } from "react-intl";
import { useConfiguration } from "src/core/StashService";
import { HoverPopover, Icon, TagLink } from "../Shared";
import { BasicCard } from "../Shared/BasicCard";

interface IProps {
gallery: GQL.GalleryDataFragment;
Expand Down Expand Up @@ -137,61 +138,13 @@ export const GalleryCard: React.FC<IProps> = (props) => {
);
}

function handleImageClick(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) {
const { shiftKey } = event;

if (props.selecting) {
props.onSelectedChanged(!props.selected, shiftKey);
event.preventDefault();
}
}

function handleDrag(event: React.DragEvent<HTMLAnchorElement>) {
if (props.selecting) {
event.dataTransfer.setData("text/plain", "");
event.dataTransfer.setDragImage(new Image(), 0, 0);
}
}

function handleDragOver(event: React.DragEvent<HTMLAnchorElement>) {
const ev = event;
const shiftKey = false;

if (props.selecting && !props.selected) {
props.onSelectedChanged(true, shiftKey);
}

ev.dataTransfer.dropEffect = "move";
ev.preventDefault();
}

let shiftKey = false;

return (
<Card className={`gallery-card zoom-${props.zoomIndex}`}>
<Form.Control
type="checkbox"
className="gallery-card-check"
checked={props.selected}
onChange={() => props.onSelectedChanged(!props.selected, shiftKey)}
onClick={(event: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
// eslint-disable-next-line prefer-destructuring
shiftKey = event.shiftKey;
event.stopPropagation();
}}
/>

<div className="gallery-section">
<Link
to={`/galleries/${props.gallery.id}`}
className="gallery-card-header"
onClick={handleImageClick}
onDragStart={handleDrag}
onDragOver={handleDragOver}
draggable={props.selecting}
>
<BasicCard
className={`gallery-card zoom-${props.zoomIndex}`}
url={`/galleries/${props.gallery.id}`}
linkClassName="gallery-card-header"
image={
<>
{props.gallery.cover ? (
<img
className="gallery-card-image"
Expand All @@ -200,26 +153,31 @@ export const GalleryCard: React.FC<IProps> = (props) => {
/>
) : undefined}
{maybeRenderRatingBanner()}
</Link>
{maybeRenderSceneStudioOverlay()}
</div>
<div className="card-section">
<Link to={`/galleries/${props.gallery.id}`}>
<h5 className="card-section-title">
{props.gallery.title ?? props.gallery.path}
</h5>
</Link>
<span>
{props.gallery.images.length}&nbsp;
<FormattedPlural
value={props.gallery.images.length ?? 0}
one="image"
other="images"
/>
.
</span>
</div>
{maybeRenderPopoverButtonGroup()}
</Card>
</>
}
overlays={maybeRenderSceneStudioOverlay()}
details={
<>
<Link to={`/galleries/${props.gallery.id}`}>
<h5 className="card-section-title">
{props.gallery.title ?? props.gallery.path}
</h5>
</Link>
<span>
{props.gallery.images.length}&nbsp;
<FormattedPlural
value={props.gallery.images.length ?? 0}
one="image"
other="images"
/>
.
</span>
</>
}
popovers={maybeRenderPopoverButtonGroup()}
selected={props.selected}
selecting={props.selecting}
onSelectedChanged={props.onSelectedChanged}
/>
);
};
Loading