From 55aee21cff33c0895c7d08e7b76d4daaf574fea2 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 11 Mar 2021 12:56:34 +1100 Subject: [PATCH] Upload Image from url (#1193) --- pkg/api/resolver_mutation_movie.go | 10 +- pkg/api/resolver_mutation_performer.go | 4 +- pkg/api/resolver_mutation_scene.go | 2 +- pkg/api/resolver_mutation_studio.go | 4 +- pkg/api/resolver_mutation_tag.go | 4 +- pkg/utils/image.go | 57 ++++++++- .../src/components/Changelog/versions/v060.md | 1 + .../components/Movies/MovieDetails/Movie.tsx | 2 + .../PerformerDetails/PerformerEditPanel.tsx | 17 ++- .../Scenes/SceneDetails/SceneEditPanel.tsx | 6 +- .../components/Shared/DetailsEditNavbar.tsx | 4 + ui/v2.5/src/components/Shared/ImageInput.tsx | 119 ++++++++++++++++-- .../Studios/StudioDetails/Studio.tsx | 1 + 13 files changed, 205 insertions(+), 26 deletions(-) diff --git a/pkg/api/resolver_mutation_movie.go b/pkg/api/resolver_mutation_movie.go index 320ed49da..3672fd47e 100644 --- a/pkg/api/resolver_mutation_movie.go +++ b/pkg/api/resolver_mutation_movie.go @@ -26,7 +26,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr // Process the base 64 encoded image string if input.FrontImage != nil { - _, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage) + frontimageData, err = utils.ProcessImageInput(*input.FrontImage) if err != nil { return nil, err } @@ -34,7 +34,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr // Process the base 64 encoded image string if input.BackImage != nil { - _, backimageData, err = utils.ProcessBase64Image(*input.BackImage) + backimageData, err = utils.ProcessImageInput(*input.BackImage) if err != nil { return nil, err } @@ -126,7 +126,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp var frontimageData []byte frontImageIncluded := translator.hasField("front_image") if input.FrontImage != nil { - _, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage) + frontimageData, err = utils.ProcessImageInput(*input.FrontImage) if err != nil { return nil, err } @@ -134,7 +134,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp backImageIncluded := translator.hasField("back_image") var backimageData []byte if input.BackImage != nil { - _, backimageData, err = utils.ProcessBase64Image(*input.BackImage) + backimageData, err = utils.ProcessImageInput(*input.BackImage) if err != nil { return nil, err } @@ -189,7 +189,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp // HACK - if front image is null and back image is not null, then set the front image // to the default image since we can't have a null front image and a non-null back image if frontimageData == nil && backimageData != nil { - _, frontimageData, _ = utils.ProcessBase64Image(models.DefaultMovieImage) + frontimageData, _ = utils.ProcessImageInput(models.DefaultMovieImage) } if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil { diff --git a/pkg/api/resolver_mutation_performer.go b/pkg/api/resolver_mutation_performer.go index e0056efd7..69eb5832c 100644 --- a/pkg/api/resolver_mutation_performer.go +++ b/pkg/api/resolver_mutation_performer.go @@ -18,7 +18,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per var err error if input.Image != nil { - _, imageData, err = utils.ProcessBase64Image(*input.Image) + imageData, err = utils.ProcessImageInput(*input.Image) } if err != nil { @@ -139,7 +139,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per var err error imageIncluded := translator.hasField("image") if input.Image != nil { - _, imageData, err = utils.ProcessBase64Image(*input.Image) + imageData, err = utils.ProcessImageInput(*input.Image) if err != nil { return nil, err } diff --git a/pkg/api/resolver_mutation_scene.go b/pkg/api/resolver_mutation_scene.go index 36e2759f4..ba73aecb1 100644 --- a/pkg/api/resolver_mutation_scene.go +++ b/pkg/api/resolver_mutation_scene.go @@ -80,7 +80,7 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator if input.CoverImage != nil && *input.CoverImage != "" { var err error - _, coverImageData, err = utils.ProcessBase64Image(*input.CoverImage) + coverImageData, err = utils.ProcessImageInput(*input.CoverImage) if err != nil { return nil, err } diff --git a/pkg/api/resolver_mutation_studio.go b/pkg/api/resolver_mutation_studio.go index bf1b5f312..82be5d1e9 100644 --- a/pkg/api/resolver_mutation_studio.go +++ b/pkg/api/resolver_mutation_studio.go @@ -20,7 +20,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio // Process the base 64 encoded image string if input.Image != nil { - _, imageData, err = utils.ProcessBase64Image(*input.Image) + imageData, err = utils.ProcessImageInput(*input.Image) if err != nil { return nil, err } @@ -96,7 +96,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio imageIncluded := translator.hasField("image") if input.Image != nil { var err error - _, imageData, err = utils.ProcessBase64Image(*input.Image) + imageData, err = utils.ProcessImageInput(*input.Image) if err != nil { return nil, err } diff --git a/pkg/api/resolver_mutation_tag.go b/pkg/api/resolver_mutation_tag.go index 90ee479d1..df11e06d0 100644 --- a/pkg/api/resolver_mutation_tag.go +++ b/pkg/api/resolver_mutation_tag.go @@ -24,7 +24,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate var err error if input.Image != nil { - _, imageData, err = utils.ProcessBase64Image(*input.Image) + imageData, err = utils.ProcessImageInput(*input.Image) if err != nil { return nil, err @@ -82,7 +82,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate imageIncluded := translator.hasField("image") if input.Image != nil { - _, imageData, err = utils.ProcessBase64Image(*input.Image) + imageData, err = utils.ProcessImageInput(*input.Image) if err != nil { return nil, err diff --git a/pkg/utils/image.go b/pkg/utils/image.go index 7c550a0be..7958f1d4e 100644 --- a/pkg/utils/image.go +++ b/pkg/utils/image.go @@ -4,11 +4,66 @@ import ( "crypto/md5" "encoding/base64" "fmt" + "io/ioutil" "net/http" "regexp" "strings" + "time" ) +// Timeout to get the image. Includes transfer time. May want to make this +// configurable at some point. +const imageGetTimeout = time.Second * 60 + +const base64RE = `^data:.+\/(.+);base64,(.*)$` + +// ProcessImageInput transforms an image string either from a base64 encoded +// string, or from a URL, and returns the image as a byte slice +func ProcessImageInput(imageInput string) ([]byte, error) { + regex := regexp.MustCompile(base64RE) + if regex.MatchString(imageInput) { + _, d, err := ProcessBase64Image(imageInput) + return d, err + } + + // assume input is a URL. Read it. + return ReadImageFromURL(imageInput) +} + +// ReadImageFromURL returns image data from a URL +func ReadImageFromURL(url string) ([]byte, error) { + client := &http.Client{ + Timeout: imageGetTimeout, + } + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + // assume is a URL for now + + // set the host of the URL as the referer + if req.URL.Scheme != "" { + req.Header.Set("Referer", req.URL.Scheme+"://"+req.Host+"/") + } + + resp, err := client.Do(req) + + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return body, nil +} + // ProcessBase64Image transforms a base64 encoded string from a form post and returns the MD5 hash of the data and the // image itself as a byte slice. func ProcessBase64Image(imageString string) (string, []byte, error) { @@ -16,7 +71,7 @@ func ProcessBase64Image(imageString string) (string, []byte, error) { return "", nil, fmt.Errorf("empty image string") } - regex := regexp.MustCompile(`^data:.+\/(.+);base64,(.*)$`) + regex := regexp.MustCompile(base64RE) matches := regex.FindStringSubmatch(imageString) var encodedString string if len(matches) > 2 { diff --git a/ui/v2.5/src/components/Changelog/versions/v060.md b/ui/v2.5/src/components/Changelog/versions/v060.md index 4d9c8b66c..811fae3d3 100644 --- a/ui/v2.5/src/components/Changelog/versions/v060.md +++ b/ui/v2.5/src/components/Changelog/versions/v060.md @@ -2,6 +2,7 @@ * Added Performer tags. ### 🎨 Improvements +* Allow scene/performer/studio image upload via URL. * Add button to hide unmatched scenes in Tagger view. * Hide create option in dropdowns when searching in filters. * Add scrape gallery from fragment to UI diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index 9f57a74bf..bd95459af 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -571,8 +571,10 @@ export const Movie: React.FC = () => { onToggleEdit={onToggleEdit} onSave={onSave} onImageChange={onFrontImageChange} + onImageChangeURL={onFrontImageLoad} onClearImage={onClearFrontImage} onBackImageChange={onBackImageChange} + onBackImageChangeURL={onBackImageLoad} onClearBackImage={onClearBackImage} onDelete={onDelete} /> diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index b197d0a6b..1f6fcad40 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -418,6 +418,10 @@ export const PerformerEditPanel: React.FC = ({ ImageUtils.onImageChange(event, onImageLoad); } + function onImageChangeURL(url: string) { + formik.setFieldValue("image", url); + } + function onDisplayScrapeDialog(scraper: GQL.Scraper) { setIsDisplayingScraperDialog(scraper); } @@ -533,7 +537,12 @@ export const PerformerEditPanel: React.FC = ({ ); return ( - + @@ -636,7 +645,11 @@ export const PerformerEditPanel: React.FC = ({ "" )} {renderScraperMenu()} - + + + + ); + } + + function onConfirmURL() { + if (!onImageURL) { + return; + } + + setIsShowDialog(false); + onImageURL(url); + } + + function renderDialog() { + return ( + setIsShowDialog(false)} + header="Image URL" + accept={{ onClick: onConfirmURL, text: "Confirm" }} + > +
+ + + URL + + + ) => + setURL(event.currentTarget.value) + } + value={url} + placeholder="URL" + /> + + +
+
+ ); + } + + const popover = ( + + + <> +
+ + + + +
+
+ +
+ +
+
+ ); + return ( - - - - + <> + {renderDialog()} + + + + ); }; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 9a79551c5..42b7b6119 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -319,6 +319,7 @@ export const Studio: React.FC = () => { onToggleEdit={onToggleEdit} onSave={onSave} onImageChange={onImageChangeHandler} + onImageChangeURL={onImageLoad} onClearImage={() => { onClearImage(); }}