Compare commits

..

2 Commits

Author SHA1 Message Date
DogmaDragon
0df4077ace Fix typo 2025-04-01 13:21:24 +03:00
DogmaDragon
b14b2796f9 Update scraper objects 2025-03-31 14:49:45 +03:00
40 changed files with 240 additions and 528 deletions

View File

@@ -16,7 +16,7 @@ env:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2

View File

@@ -5,6 +5,7 @@
[![GitHub Sponsors](https://img.shields.io/github/sponsors/stashapp?logo=github)](https://github.com/sponsors/stashapp)
[![Open Collective backers](https://img.shields.io/opencollective/backers/stashapp?logo=opencollective)](https://opencollective.com/stashapp)
[![Go Report Card](https://goreportcard.com/badge/github.com/stashapp/stash)](https://goreportcard.com/report/github.com/stashapp/stash)
[![Matrix](https://img.shields.io/matrix/stashapp:unredacted.org?logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#stashapp:unredacted.org)
[![Discord](https://img.shields.io/discord/559159668438728723.svg?logo=discord)](https://discord.gg/2TsNFKt)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest)
[![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](https://github.com/stashapp/stash/labels/bounty)
@@ -67,24 +68,19 @@ Stash is available in 32 languages (so far!) and it could be in your language to
[![Translation status](https://translate.codeberg.org/widget/stash/stash/multi-auto.svg)](https://translate.codeberg.org/engage/stash/)
## Join Our Community
We are excited to announce that we have a new home for support, feature requests, and discussions related to Stash and its associated projects. Join our community on the [Discourse forum](https://discourse.stashapp.cc) to connect with other users, share your ideas, and get help from fellow enthusiasts.
# Support (FAQ)
Check out our documentation on [Stash-Docs](https://docs.stashapp.cc) for information about the software, questions, guides, add-ons and more.
For more help you can:
* Check the in-app documentation, in the top right corner of the app (it's also mirrored on [Stash-Docs](https://docs.stashapp.cc/in-app-manual))
* Join our [community forum](https://discourse.stashapp.cc)
* Join the [Discord server](https://discord.gg/2TsNFKt)
* Join the [Matrix space](https://matrix.to/#/#stashapp:unredacted.org)
* Join the [Discord server](https://discord.gg/2TsNFKt), where the community can offer support.
* Start a [discussion on GitHub](https://github.com/stashapp/stash/discussions)
# Customization
## Themes and CSS Customization
There is a [directory of community-created themes](https://docs.stashapp.cc/themes/list) on Stash-Docs.
You can also change the Stash interface to fit your desired style with various snippets from [Custom CSS snippets](https://docs.stashapp.cc/themes/custom-css-snippets).

View File

@@ -152,9 +152,6 @@ func recoverPanic() {
func exitError(err error) {
exitCode = 1
logger.Error(err)
// #5784 - log to stdout as well as the logger
// this does mean that it will log twice if the logger is set to stdout
fmt.Println(err)
if desktop.IsDesktop() {
desktop.FatalError(err)
}

View File

@@ -1,5 +1,4 @@
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
ARG CUDA_VERSION=12.8.0
# Build Frontend
FROM node:20-alpine AS frontend
@@ -35,26 +34,19 @@ ARG STASH_VERSION
RUN make flags-release flags-pie stash
# Final Runnable Image
FROM nvidia/cuda:${CUDA_VERSION}-base-ubuntu24.04
RUN apt update && apt upgrade -y && apt install -y \
# stash dependencies
ca-certificates libvips-tools ffmpeg \
# intel dependencies
intel-media-va-driver-non-free vainfo \
# python tools
python3 python3-pip && \
# cleanup
apt autoremove -y && apt clean && \
rm -rf /var/lib/apt/lists/*
COPY --from=backend --chmod=555 /stash/stash /usr/bin/
FROM nvidia/cuda:12.0.1-base-ubuntu22.04
RUN apt update && apt upgrade -y && apt install -y ca-certificates libvips-tools ffmpeg wget intel-media-va-driver-non-free vainfo
RUN rm -rf /var/lib/apt/lists/*
COPY --from=backend /stash/stash /usr/bin/
# NVENC Patch
RUN mkdir -p /usr/local/bin /patched-lib
ADD --chmod=555 https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh /usr/local/bin/patch.sh
ADD --chmod=555 https://raw.githubusercontent.com/keylase/nvidia-patch/master/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh -O /usr/local/bin/patch.sh
RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/docker-entrypoint.sh -O /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/patch.sh /usr/local/bin/docker-entrypoint.sh /usr/bin/stash
ENV LANG=C.UTF-8
ENV NVIDIA_VISIBLE_DEVICES=all
ENV LANG C.UTF-8
ENV NVIDIA_VISIBLE_DEVICES all
ENV NVIDIA_DRIVER_CAPABILITIES=video,utility
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
EXPOSE 9999

View File

@@ -16,12 +16,12 @@ import (
const (
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
"More information and fixes are available at https://discourse.stashapp.cc/t/-/1658"
"More information and fixes are available at https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " +
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
"Stash is not answering any other requests to protect your privacy. " +
"Please read the log entry or visit https://discourse.stashapp.cc/t/-/1658"
"Please read the log entry or visit https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
)
func allowUnauthenticated(r *http.Request) bool {

View File

@@ -694,13 +694,6 @@ func validateSceneMarkerEndSeconds(seconds, endSeconds float64) error {
return nil
}
func float64OrZero(f *float64) float64 {
if f == nil {
return 0
}
return *f
}
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
markerID, err := strconv.Atoi(input.ID)
if err != nil {
@@ -791,7 +784,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
}
// remove the marker preview if the scene changed or if the timestamp was changed
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds || float64OrZero(existingMarker.EndSeconds) != float64OrZero(newMarker.EndSeconds) {
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds || existingMarker.EndSeconds != newMarker.EndSeconds {
seconds := int(existingMarker.Seconds)
if err := fileDeleter.MarkMarkerFiles(existingScene, seconds); err != nil {
return err

View File

@@ -29,7 +29,7 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input
var scenes []*models.Scene
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
scenes, err = r.sceneService.FindByIDs(ctx, ids, scene.LoadStashIDs, scene.LoadFiles)
scenes, err = r.sceneService.FindMany(ctx, ids, scene.LoadStashIDs, scene.LoadFiles)
return err
}); err != nil {
return false, err

View File

@@ -62,11 +62,7 @@ func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilte
func (r *queryResolver) AllTags(ctx context.Context) (ret []*models.Tag, err error) {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.All(ctx)
if err != nil {
return err
}
return nil
return err
}); err != nil {
return nil, err
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"slices"
"strconv"
"github.com/stashapp/stash/pkg/match"
@@ -101,12 +100,12 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
}
func (r *queryResolver) ScrapeGroupURL(ctx context.Context, url string) (*models.ScrapedGroup, error) {
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeGroup)
content, err := r.scraperCache().ScrapeURL(ctx, url, scraper.ScrapeContentTypeMovie)
if err != nil {
return nil, err
}
ret, err := marshalScrapedGroup(content)
ret, err := marshalScrapedMovie(content)
if err != nil {
return nil, err
}
@@ -208,10 +207,6 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
return nil, fmt.Errorf("%w: scraper_id or stash_box_index must be set", ErrInput)
}
for i := range ret {
slices.SortFunc(ret[i].Tags, models.ScrapedTagSortFunction)
}
return ret, nil
}

View File

@@ -113,30 +113,7 @@ func marshalScrapedMovies(content []scraper.ScrapedContent) ([]*models.ScrapedMo
case models.ScrapedMovie:
ret = append(ret, &m)
default:
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedMovie", models.ErrConversion)
}
}
return ret, nil
}
// marshalScrapedMovies converts ScrapedContent into ScrapedMovie. If conversion
// fails, an error is returned.
func marshalScrapedGroups(content []scraper.ScrapedContent) ([]*models.ScrapedGroup, error) {
var ret []*models.ScrapedGroup
for _, c := range content {
if c == nil {
// graphql schema requires groups to be non-nil
continue
}
switch m := c.(type) {
case *models.ScrapedGroup:
ret = append(ret, m)
case models.ScrapedGroup:
ret = append(ret, &m)
default:
return nil, fmt.Errorf("%w: cannot turn ScrapedContent into ScrapedGroup", models.ErrConversion)
return nil, fmt.Errorf("%w: cannot turn ScrapedConetnt into ScrapedMovie", models.ErrConversion)
}
}
@@ -192,13 +169,3 @@ func marshalScrapedMovie(content scraper.ScrapedContent) (*models.ScrapedMovie,
return m[0], nil
}
// marshalScrapedMovie will marshal a single scraped movie
func marshalScrapedGroup(content scraper.ScrapedContent) (*models.ScrapedGroup, error) {
m, err := marshalScrapedGroups([]scraper.ScrapedContent{content})
if err != nil {
return nil, err
}
return m[0], nil
}

View File

@@ -1534,7 +1534,7 @@ func (i *Config) GetDefaultGenerateSettings() *models.GenerateMetadataOptions {
}
// GetDangerousAllowPublicWithoutAuth determines if the security feature is enabled.
// See https://discourse.stashapp.cc/t/-/1658
// See https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet
func (i *Config) GetDangerousAllowPublicWithoutAuth() bool {
return i.getBool(dangerousAllowPublicWithoutAuth)
}

View File

@@ -15,7 +15,7 @@ type SceneService interface {
Merge(ctx context.Context, sourceIDs []int, destinationID int, fileDeleter *scene.FileDeleter, options scene.MergeOptions) error
Destroy(ctx context.Context, scene *models.Scene, fileDeleter *scene.FileDeleter, deleteGenerated, deleteFile bool) error
FindByIDs(ctx context.Context, ids []int, load ...scene.LoadRelationshipOption) ([]*models.Scene, error)
FindMany(ctx context.Context, ids []int, load ...scene.LoadRelationshipOption) ([]*models.Scene, error)
sceneFingerprintGetter
}

View File

@@ -1042,43 +1042,23 @@ func (t *ExportTask) ExportTags(ctx context.Context, workers int) {
logger.Info("[tags] exporting")
startTime := time.Now()
tagIdx := 0
if t.tags != nil {
tagIdx = len(t.tags.IDs)
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)
}
for {
jobCh := make(chan *models.Tag, workers*2) // make a buffered channel to feed workers
for i, tag := range tags {
index := i + 1
logger.Progressf("[tags] %d of %d", index, len(tags))
for w := 0; w < workers; w++ { // create export Tag workers
tagsWg.Add(1)
go t.exportTag(ctx, &tagsWg, jobCh)
}
for i, tag := range tags {
index := i + 1 + tagIdx
logger.Progressf("[tags] %d of %d", index, len(tags)+tagIdx)
jobCh <- tag // feed workers
}
close(jobCh)
tagsWg.Wait()
// if more tags were added, we need to export those too
if t.tags == nil || len(t.tags.IDs) == tagIdx {
break
}
newTags, err := reader.FindMany(ctx, t.tags.IDs[tagIdx:])
if err != nil {
logger.Errorf("[tags] failed to fetch tags: %v", err)
}
tags = newTags
tagIdx = len(t.tags.IDs)
jobCh <- tag // feed workers
}
close(jobCh)
tagsWg.Wait()
logger.Infof("[tags] export complete in %s. %d workers used.", time.Since(startTime), workers)
}
@@ -1095,15 +1075,6 @@ func (t *ExportTask) exportTag(ctx context.Context, wg *sync.WaitGroup, jobChan
continue
}
if t.includeDependencies {
tagIDs, err := tag.GetDependentTagIDs(ctx, tagReader, thisTag)
if err != nil {
logger.Errorf("[tags] <%s> error getting dependent tags: %v", thisTag.Name, err)
continue
}
t.tags.IDs = sliceutil.AppendUniques(t.tags.IDs, tagIDs)
}
fn := newTagJSON.Filename()
if err := t.json.saveTag(fn, newTagJSON); err != nil {

View File

@@ -426,11 +426,9 @@ func serveHLSManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request,
return
}
prefix := r.Header.Get("X-Forwarded-Prefix")
baseUrl := *r.URL
baseUrl.RawQuery = ""
baseURL := prefix + baseUrl.String()
baseURL := baseUrl.String()
urlQuery := url.Values{}
apikey := r.URL.Query().Get(apiKeyParamKey)
@@ -561,11 +559,9 @@ func serveDASHManifest(sm *StreamManager, w http.ResponseWriter, r *http.Request
mediaDuration := mpd.Duration(time.Duration(probeResult.FileDuration * float64(time.Second)))
m := mpd.NewMPD(mpd.DASH_PROFILE_LIVE, mediaDuration.String(), "PT4.0S")
prefix := r.Header.Get("X-Forwarded-Prefix")
baseUrl := r.URL.JoinPath("/")
baseUrl.RawQuery = ""
m.BaseURL = prefix + baseUrl.String()
m.BaseURL = baseUrl.String()
video, _ := m.AddNewAdaptationSetVideo(MimeWebmVideo, "progressive", true, 1)

View File

@@ -1,8 +1,6 @@
package fsutil
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"os"
@@ -153,12 +151,7 @@ var (
)
// SanitiseBasename returns a file basename removing any characters that are illegal or problematic to use in the filesystem.
// It appends a short hash of the original string to ensure uniqueness.
func SanitiseBasename(v string) string {
// Generate a short hash for uniqueness
hash := sha1.Sum([]byte(v))
shortHash := hex.EncodeToString(hash[:4]) // Use the first 4 bytes of the hash
v = strings.TrimSpace(v)
// replace illegal filename characters with -
@@ -170,7 +163,7 @@ func SanitiseBasename(v string) string {
// remove multiple hyphens
v = multiHyphenRE.ReplaceAllString(v, "-")
return strings.TrimSpace(v) + "-" + shortHash
return strings.TrimSpace(v)
}
// GetExeName returns the name of the given executable for the current platform.

View File

@@ -8,13 +8,13 @@ func TestSanitiseBasename(t *testing.T) {
v string
want string
}{
{"basic", "basic", "basic-61a7508e"},
{"spaces", `spaced name`, "spaced-name-b297cf60"},
{"leading/trailing spaces", ` spaced name `, "spaced-name-175433e9"},
{"hyphen name", `hyphened-name`, "hyphened-name-789c55f2"},
{"multi-hyphen", `hyphened--name`, "hyphened-name-2da2a58f"},
{"replaced characters", `a&b=c\d/:e*"f?_ g`, "a-b-c-d-e-f-g-ffca6fb0"},
{"removed characters", `foo!!bar@@and, more`, "foobarand-more-7cee02ab"},
{"basic", "basic", "basic"},
{"spaces", `spaced name`, "spaced-name"},
{"leading/trailing spaces", ` spaced name `, "spaced-name"},
{"hyphen name", `hyphened-name`, "hyphened-name"},
{"multi-hyphen", `hyphened--name`, "hyphened-name"},
{"replaced characters", `a&b=c\d/:e*"f?_ g`, "a-b-c-d-e-f-g"},
{"removed characters", `foo!!bar@@and, more`, "foobarand-more"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -30,7 +30,7 @@ type SceneRelationships struct {
func (r SceneRelationships) MatchRelationships(ctx context.Context, s *models.ScrapedScene, endpoint string) error {
thisStudio := s.Studio
for thisStudio != nil {
if err := ScrapedStudio(ctx, r.StudioFinder, thisStudio, endpoint); err != nil {
if err := ScrapedStudio(ctx, r.StudioFinder, s.Studio, endpoint); err != nil {
return err
}

View File

@@ -549,29 +549,6 @@ func (_m *SceneReaderWriter) FindByGroupID(ctx context.Context, groupID int) ([]
return r0, r1
}
// FindByIDs provides a mock function with given fields: ctx, ids
func (_m *SceneReaderWriter) FindByIDs(ctx context.Context, ids []int) ([]*models.Scene, error) {
ret := _m.Called(ctx, ids)
var r0 []*models.Scene
if rf, ok := ret.Get(0).(func(context.Context, []int) []*models.Scene); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Scene)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindByOSHash provides a mock function with given fields: ctx, oshash
func (_m *SceneReaderWriter) FindByOSHash(ctx context.Context, oshash string) ([]*models.Scene, error) {
ret := _m.Called(ctx, oshash)

View File

@@ -18,10 +18,6 @@ func (s *sceneResolver) FindMany(ctx context.Context, ids []int) ([]*models.Scen
return s.scenes, nil
}
func (s *sceneResolver) FindByIDs(ctx context.Context, ids []int) ([]*models.Scene, error) {
return s.scenes, nil
}
func SceneQueryResult(scenes []*models.Scene, count int) *models.SceneQueryResult {
ret := models.NewSceneQueryResult(&sceneResolver{
scenes: scenes,

View File

@@ -3,7 +3,6 @@ package models
import (
"context"
"strconv"
"strings"
"time"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
@@ -401,10 +400,6 @@ type ScrapedTag struct {
func (ScrapedTag) IsScrapedContent() {}
func ScrapedTagSortFunction(a, b *ScrapedTag) int {
return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
}
// A movie from a scraping operation...
type ScrapedMovie struct {
StoredID *string `json:"stored_id"`

View File

@@ -10,9 +10,6 @@ type SceneGetter interface {
// TODO - rename this to Find and remove existing method
FindMany(ctx context.Context, ids []int) ([]*Scene, error)
Find(ctx context.Context, id int) (*Scene, error)
// FindByIDs works the same way as FindMany, but it ignores any scenes not found
// Scenes are not guaranteed to be in the same order as the input
FindByIDs(ctx context.Context, ids []int) ([]*Scene, error)
}
// SceneFinder provides methods to find scenes.

View File

@@ -33,31 +33,7 @@ func LoadFiles(ctx context.Context, scene *models.Scene, r models.SceneReader) e
return nil
}
// FindByIDs retrieves multiple scenes by their IDs.
// Missing scenes will be ignored, and the returned scenes are unsorted.
// This method will load the specified relationships for each scene.
func (s *Service) FindByIDs(ctx context.Context, ids []int, load ...LoadRelationshipOption) ([]*models.Scene, error) {
var scenes []*models.Scene
qb := s.Repository
var err error
scenes, err = qb.FindByIDs(ctx, ids)
if err != nil {
return nil, err
}
// TODO - we should bulk load these relationships
for _, scene := range scenes {
if err := s.LoadRelationships(ctx, scene, load...); err != nil {
return nil, err
}
}
return scenes, nil
}
// FindMany retrieves multiple scenes by their IDs. Return value is guaranteed to be in the same order as the input.
// Missing scenes will return an error.
// FindMany retrieves multiple scenes by their IDs.
// This method will load the specified relationships for each scene.
func (s *Service) FindMany(ctx context.Context, ids []int, load ...LoadRelationshipOption) ([]*models.Scene, error) {
var scenes []*models.Scene

View File

@@ -378,11 +378,6 @@ func (c config) matchesURL(url string, ty ScrapeContentType) bool {
}
}
case ScrapeContentTypeMovie, ScrapeContentTypeGroup:
for _, scraper := range c.GroupByURL {
if scraper.matchesURL(url) {
return true
}
}
for _, scraper := range c.MovieByURL {
if scraper.matchesURL(url) {
return true

View File

@@ -851,10 +851,7 @@ type mappedScraper struct {
Gallery *mappedGalleryScraperConfig `yaml:"gallery"`
Image *mappedImageScraperConfig `yaml:"image"`
Performer *mappedPerformerScraperConfig `yaml:"performer"`
Group *mappedMovieScraperConfig `yaml:"group"`
// deprecated
Movie *mappedMovieScraperConfig `yaml:"movie"`
Movie *mappedMovieScraperConfig `yaml:"movie"`
}
type mappedResult map[string]interface{}
@@ -1250,29 +1247,24 @@ func (s mappedScraper) scrapeGallery(ctx context.Context, q mappedQuery) (*model
return &ret, nil
}
func (s mappedScraper) scrapeGroup(ctx context.Context, q mappedQuery) (*models.ScrapedGroup, error) {
var ret models.ScrapedGroup
func (s mappedScraper) scrapeGroup(ctx context.Context, q mappedQuery) (*models.ScrapedMovie, error) {
var ret models.ScrapedMovie
// try group scraper first, falling back to movie
groupScraperConfig := s.Group
if groupScraperConfig == nil {
groupScraperConfig = s.Movie
}
if groupScraperConfig == nil {
movieScraperConfig := s.Movie
if movieScraperConfig == nil {
return nil, nil
}
groupMap := groupScraperConfig.mappedConfig
movieMap := movieScraperConfig.mappedConfig
groupStudioMap := groupScraperConfig.Studio
groupTagsMap := groupScraperConfig.Tags
movieStudioMap := movieScraperConfig.Studio
movieTagsMap := movieScraperConfig.Tags
results := groupMap.process(ctx, q, s.Common, urlsIsMulti)
results := movieMap.process(ctx, q, s.Common, urlsIsMulti)
if groupStudioMap != nil {
logger.Debug(`Processing group studio:`)
studioResults := groupStudioMap.process(ctx, q, s.Common, nil)
if movieStudioMap != nil {
logger.Debug(`Processing movie studio:`)
studioResults := movieStudioMap.process(ctx, q, s.Common, nil)
if len(studioResults) > 0 {
studio := &models.ScrapedStudio{}
@@ -1282,9 +1274,9 @@ func (s mappedScraper) scrapeGroup(ctx context.Context, q mappedQuery) (*models.
}
// now apply the tags
if groupTagsMap != nil {
logger.Debug(`Processing group tags:`)
tagResults := groupTagsMap.process(ctx, q, s.Common, nil)
if movieTagsMap != nil {
logger.Debug(`Processing movie tags:`)
tagResults := movieTagsMap.process(ctx, q, s.Common, nil)
for _, p := range tagResults {
tag := &models.ScrapedTag{}

View File

@@ -32,8 +32,6 @@ func FilterTags(excludeRegexps []*regexp.Regexp, tags []*models.ScrapedTag) (new
return tags, nil
}
newTags = make([]*models.ScrapedTag, 0, len(tags))
for _, t := range tags {
ignore := false
for _, reg := range excludeRegexps {

View File

@@ -81,6 +81,6 @@ func LogExternalAccessError(err ExternalAccessError) {
"You probably forwarded a port from your router. At the very least, add a password to stash in the settings. \n"+
"Stash will not serve requests until you edit config.yml, remove the security_tripwire_accessed_from_public_internet key and restart stash. \n"+
"This behaviour can be overridden (but not recommended) by setting dangerous_allow_public_without_auth to true in config.yml. \n"+
"More information is available at https://discourse.stashapp.cc/t/-/1658 \n"+
"More information is available at https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet \n"+
"Stash is not answering any other requests to protect your privacy.", net.IP(err).String())
}

View File

@@ -493,11 +493,8 @@ func (qb *SceneStore) Find(ctx context.Context, id int) (*models.Scene, error) {
return ret, err
}
// FindByIDs finds multiple scenes by their IDs.
// No check is made to see if the scenes exist, and the order of the returned scenes
// is not guaranteed to be the same as the order of the input IDs.
func (qb *SceneStore) FindByIDs(ctx context.Context, ids []int) ([]*models.Scene, error) {
scenes := make([]*models.Scene, 0, len(ids))
func (qb *SceneStore) FindMany(ctx context.Context, ids []int) ([]*models.Scene, error) {
scenes := make([]*models.Scene, len(ids))
table := qb.table()
if err := batchExec(ids, defaultBatchSize, func(batch []int) error {
@@ -507,29 +504,16 @@ func (qb *SceneStore) FindByIDs(ctx context.Context, ids []int) ([]*models.Scene
return err
}
scenes = append(scenes, unsorted...)
for _, s := range unsorted {
i := slices.Index(ids, s.ID)
scenes[i] = s
}
return nil
}); err != nil {
return nil, err
}
return scenes, nil
}
func (qb *SceneStore) FindMany(ctx context.Context, ids []int) ([]*models.Scene, error) {
scenes := make([]*models.Scene, len(ids))
unsorted, err := qb.FindByIDs(ctx, ids)
if err != nil {
return nil, err
}
for _, s := range unsorted {
i := slices.Index(ids, s.ID)
scenes[i] = s
}
for i := range scenes {
if scenes[i] == nil {
return nil, fmt.Errorf("scene with id %d not found", ids[i])

View File

@@ -562,7 +562,7 @@ func (qb *TagStore) All(ctx context.Context) ([]*models.Tag, error) {
table := qb.table()
return qb.getMany(ctx, qb.selectDataset().Order(
goqu.L("COALESCE(tags.sort_name, tags.name) COLLATE NATURAL_CI").Asc(),
table.Col("name").Asc(),
table.Col(idColumn).Asc(),
))
}

View File

@@ -1018,7 +1018,6 @@ type StashBoxConfig struct {
GuidelinesURL string `json:"guidelines_url"`
RequireSceneDraft bool `json:"require_scene_draft"`
EditUpdateLimit int `json:"edit_update_limit"`
RequireTagRole bool `json:"require_tag_role"`
}
type StringCriterionInput struct {
@@ -2144,8 +2143,6 @@ const (
// May grant and rescind invite tokens and resind invite keys
RoleEnumManageInvites RoleEnum = "MANAGE_INVITES"
RoleEnumBot RoleEnum = "BOT"
RoleEnumReadOnly RoleEnum = "READ_ONLY"
RoleEnumEditTags RoleEnum = "EDIT_TAGS"
)
var AllRoleEnum = []RoleEnum{
@@ -2157,13 +2154,11 @@ var AllRoleEnum = []RoleEnum{
RoleEnumInvite,
RoleEnumManageInvites,
RoleEnumBot,
RoleEnumReadOnly,
RoleEnumEditTags,
}
func (e RoleEnum) IsValid() bool {
switch e {
case RoleEnumRead, RoleEnumVote, RoleEnumEdit, RoleEnumModify, RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites, RoleEnumBot, RoleEnumReadOnly, RoleEnumEditTags:
case RoleEnumRead, RoleEnumVote, RoleEnumEdit, RoleEnumModify, RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites, RoleEnumBot:
return true
}
return false

View File

@@ -8,7 +8,6 @@ import (
"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/sliceutil"
"github.com/stashapp/stash/pkg/utils"
)
@@ -56,28 +55,6 @@ func ToJSON(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag)
return &newTagJSON, nil
}
// GetDependentTagIDs returns a slice of unique tag IDs that this tag references.
func GetDependentTagIDs(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag) ([]int, error) {
var ret []int
parents, err := reader.FindByChildTagID(ctx, tag.ID)
if err != nil {
return nil, fmt.Errorf("error getting parents: %v", err)
}
for _, tt := range parents {
toAdd, err := GetDependentTagIDs(ctx, reader, tt)
if err != nil {
return nil, fmt.Errorf("error getting dependent tag IDs: %v", err)
}
ret = sliceutil.AppendUniques(ret, toAdd)
ret = sliceutil.AppendUnique(ret, tt.ID)
}
return ret, nil
}
func GetIDs(tags []*models.Tag) []int {
var results []int
for _, tag := range tags {

View File

@@ -128,7 +128,7 @@
"terser": "^5.9.0",
"ts-node": "^10.9.1",
"typescript": "~4.8.4",
"vite": "^4.5.11",
"vite": "^4.5.6",
"vite-plugin-compression": "^0.5.1",
"vite-tsconfig-paths": "^4.0.5"
}

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useMemo } from "react";
import { useLightbox } from "src/hooks/Lightbox/hooks";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import Gallery, { PhotoClickHandler } from "react-photo-gallery";
import Gallery from "react-photo-gallery";
import "flexbin/flexbin.css";
import {
CriterionModifier,
@@ -45,9 +45,9 @@ export const GalleryViewer: React.FC<IProps> = ({ galleryId }) => {
}, [images]);
const showLightbox = useLightbox(lightboxState);
const showLightboxOnClick: PhotoClickHandler = useCallback(
const showLightboxOnClick = useCallback(
(event, { index }) => {
showLightbox({ initialIndex: index });
showLightbox(index);
},
[showLightbox]
);

View File

@@ -1,71 +0,0 @@
import React from "react";
import { ErrorMessage } from "../Shared/ErrorMessage";
import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { HoverPopover } from "../Shared/HoverPopover";
import { useFindPerformer } from "../../core/StashService";
import { PerformerCard } from "./PerformerCard";
import { ConfigurationContext } from "../../hooks/Config";
import { Placement } from "react-bootstrap/esm/Overlay";
interface IPeromerPopoverCardProps {
id: string;
}
export const PerformerPopoverCard: React.FC<IPeromerPopoverCardProps> = ({
id,
}) => {
const { data, loading, error } = useFindPerformer(id);
if (loading)
return (
<div className="tag-popover-card-placeholder">
<LoadingIndicator card={true} message={""} />
</div>
);
if (error) return <ErrorMessage error={error.message} />;
if (!data?.findPerformer)
return <ErrorMessage error={`No tag found with id ${id}.`} />;
const performer = data.findPerformer;
return (
<div className="tag-popover-card">
<PerformerCard performer={performer} zoomIndex={0} />
</div>
);
};
interface IPeroformerPopoverProps {
id: string;
hide?: boolean;
placement?: Placement;
target?: React.RefObject<HTMLElement>;
}
export const PerformerPopover: React.FC<IPeroformerPopoverProps> = ({
id,
hide,
children,
placement = "top",
target,
}) => {
const { configuration: config } = React.useContext(ConfigurationContext);
const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true;
if (hide || !showPerformerCardOnHover) {
return <>{children}</>;
}
return (
<HoverPopover
target={target}
placement={placement}
enterDelay={500}
leaveDelay={100}
content={<PerformerPopoverCard id={id} />}
>
{children}
</HoverPopover>
);
};

View File

@@ -30,8 +30,6 @@ import { sortByRelevance } from "src/utils/query";
import { PatchComponent, PatchFunction } from "src/patch";
import { TruncatedText } from "../Shared/TruncatedText";
import TextUtils from "src/utils/text";
import { PerformerPopover } from "./PerformerPopover";
import { Placement } from "react-bootstrap/esm/Overlay";
export type SelectObject = {
id: string;
@@ -73,12 +71,7 @@ const performerSelectSort = PatchFunction(
);
const _PerformerSelect: React.FC<
IFilterProps &
IFilterValueProps<Performer> & {
ageFromDate?: string | null;
hoverPlacementLabel?: Placement;
hoverPlacementOptions?: Placement;
}
IFilterProps & IFilterValueProps<Performer> & { ageFromDate?: string | null }
> = (props) => {
const [createPerformer] = usePerformerCreate();
@@ -208,17 +201,12 @@ const _PerformerSelect: React.FC<
thisOptionProps = {
...optionProps,
children: (
<PerformerPopover
id={object.id}
placement={props.hoverPlacementLabel ?? "top"}
>
<span className="performer-select-value">
<span>{object.name}</span>
{object.disambiguation && (
<span className="performer-disambiguation">{` (${object.disambiguation})`}</span>
)}
</span>
</PerformerPopover>
<span className="performer-select-value">
<span>{object.name}</span>
{object.disambiguation && (
<span className="performer-disambiguation">{` (${object.disambiguation})`}</span>
)}
</span>
),
};

View File

@@ -0,0 +1,27 @@
.vjs-marker-dot {
position: absolute;
background-color: #10b981;
width: 8px;
height: 8px;
border-radius: 50%;
cursor: pointer;
z-index: 2;
transform: translate(-50%, -50%);
top: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: transform 0.2s ease;
}
.vjs-marker-dot:hover {
transform: translate(-50%, -50%) scale(1.2);
}
.vjs-marker-range {
position: absolute;
background-color: rgba(255, 255, 255, 0.4);
height: 8px;
border-radius: 2px;
transform: translateY(-28px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: none;
}

View File

@@ -1,4 +1,5 @@
import videojs, { VideoJsPlayer } from "video.js";
import "./markers.css";
import CryptoJS from "crypto-js";
export interface IMarker {
@@ -66,16 +67,14 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
dot?: HTMLDivElement;
range?: HTMLDivElement;
} = {};
const seekBar = this.player.el().querySelector(".vjs-progress-holder");
const seekBar = this.player.el().querySelector(".vjs-progress-control");
markerSet.dot = videojs.dom.createEl("div") as HTMLDivElement;
markerSet.dot.className = "vjs-marker";
markerSet.dot.className = "vjs-marker-dot";
if (duration) {
// marker is 6px wide - adjust by 3px to align to center not left side
markerSet.dot.style.left = `calc(${
(marker.seconds / duration) * 100
}% - 3px)`;
markerSet.dot.style.visibility = "visible";
}
// Add event listeners to dot
@@ -111,12 +110,11 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
private renderRangeMarkers(markers: IMarker[], layer: number) {
const duration = this.player.duration();
const parent = this.player.el().querySelector(".vjs-progress-control");
const seekBar = this.player.el().querySelector(".vjs-progress-holder");
if (!seekBar || !parent || !duration) return;
const seekBar = this.player.el().querySelector(".vjs-progress-control");
if (!seekBar || !duration) return;
markers.forEach((marker) => {
this.renderRangeMarker(marker, layer, duration, seekBar, parent);
this.renderRangeMarker(marker, layer, duration, seekBar);
});
}
@@ -124,8 +122,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
marker: IMarker,
layer: number,
duration: number,
seekBar: Element,
parent: Element
seekBar: Element
) {
if (!marker.end_seconds) return;
@@ -136,18 +133,16 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
const rangeDiv = videojs.dom.createEl("div") as HTMLDivElement;
rangeDiv.className = "vjs-marker-range";
// start/end percent is relative to the parent element, which is the vjs-progress-control
// vjs-progress-control has 15px margins on each side
const left = seekBar.clientWidth * (marker.seconds / duration) + 15;
// minimum width of 8px
const width = Math.max(
seekBar.clientWidth * ((marker.end_seconds - marker.seconds) / duration),
8
);
rangeDiv.style.left = `${left}px`;
rangeDiv.style.width = `${width}px`;
const startPercent = (marker.seconds / duration) * 100;
const endPercent = (marker.end_seconds / duration) * 100;
let width = endPercent - startPercent;
// Ensure the width is at least 8px
const minWidth = (10 / seekBar.clientWidth) * 100; // Convert 8px to percentage
if (width < minWidth) {
width = minWidth;
}
rangeDiv.style.left = `${startPercent}%`;
rangeDiv.style.width = `${width}%`;
rangeDiv.style.bottom = `${layer * this.layerHeight}px`; // Adjust height based on layer
rangeDiv.style.display = "none"; // Initially hidden
@@ -176,7 +171,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
this.hideMarkerTooltip();
markerSet.range?.toggleAttribute("marker-tooltip-shown", false);
});
parent.appendChild(rangeDiv);
seekBar.appendChild(rangeDiv);
this.markers.push(marker);
this.markerDivs.push(markerSet);
}

View File

@@ -266,16 +266,6 @@ $sceneTabWidth: 450px;
}
}
.vjs-marker-range {
background-color: rgba(255, 255, 255, 0.4);
border-radius: 2px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
height: 8px;
position: absolute;
transform: translateY(-28px);
transition: none;
}
.vjs-marker-tooltip {
background-color: #fff;
background-color: rgba(255, 255, 255, 0.8);

View File

@@ -36,10 +36,7 @@ export type SelectObject = {
title?: string | null;
};
export type Tag = Pick<
GQL.Tag,
"id" | "name" | "sort_name" | "aliases" | "image_path"
>;
export type Tag = Pick<GQL.Tag, "id" | "name" | "aliases" | "image_path">;
type Option = SelectOption<Tag>;
type FindTagsResult = Awaited<
@@ -60,7 +57,6 @@ const tagSelectSort = PatchFunction("TagSelect.sort", sortTagsByRelevance);
export type TagSelectProps = IFilterProps &
IFilterValueProps<Tag> & {
hoverPlacement?: Placement;
hoverPlacementLabel?: Placement;
excludeIds?: string[];
};
@@ -155,14 +151,7 @@ const _TagSelect: React.FC<TagSelectProps> = (props) => {
thisOptionProps = {
...optionProps,
children: (
<TagPopover
id={object.id}
placement={props.hoverPlacementLabel ?? "top"}
>
<span>{object.name}</span>
</TagPopover>
),
children: object.name,
};
return <reactSelectComponents.MultiValueLabel {...thisOptionProps} />;
@@ -296,20 +285,7 @@ const _TagIDSelect: React.FC<IFilterProps & IFilterIDProps<Tag>> = (props) => {
const load = async () => {
const items = await loadObjectsByID(ids);
// #4684 - sort items by sort name/name
const sortedItems = [...items];
sortedItems.sort((a, b) => {
const aName = a.sort_name || a.name;
const bName = b.sort_name || b.name;
if (aName && bName) {
return aName.localeCompare(bName);
}
return 0;
});
setValues(sortedItems);
setValues(items);
};
load();

View File

@@ -2,6 +2,10 @@
Scrapers can be contributed to the community by creating a PR in [this repository](https://github.com/stashapp/CommunityScrapers/pulls).
## XPath scraper templates
The most basic XPath scraper templates are available on [CommunityScrapers repository](https://github.com/stashapp/CommunityScrapers/tree/master/templates).
## Scraper configuration file format
```yaml
@@ -234,7 +238,6 @@ The above configuration would scrape from the value of `queryURL`, replacing `{f
### scrapeXPath and scrapeJson use with `<scene|performer|gallery|group>ByURL`
For `sceneByURL`, `performerByURL`, `galleryByURL` the `queryURL` can also be present if we want to use `queryURLReplace`. The functionality is the same as `sceneByFragment`, the only placeholder field available though is the `url`:
* `{url}` - the url of the scene/performer/gallery
```yaml
@@ -254,9 +257,7 @@ sceneByURL:
A different stash server can be configured as a scraping source. This action applies only to `performerByName`, `performerByFragment`, `sceneByName`, `sceneByQueryFragment` and `sceneByFragment`, types. This action requires that the top-level `stashServer` field is configured.
- `stashServer` contains a single `url` field for the remote stash server.
- The username and password can be embedded in this string using `username:password@host`.
- Alternatively, the `apiKey` field can be used to authenticate with the remote stash server.
`stashServer` contains a single `url` field for the remote stash server. The username and password can be embedded in this string using `username:password@host`. Alternatively, the `apiKey` field can be used to authenticate with the remote stash server.
An example stash scrape configuration is below:
@@ -355,7 +356,6 @@ scene:
### Post-processing options
Post-processing operations are contained in the `postProcess` key. Post-processing operations are performed in the order they are specified. The following post-processing operations are available:
* `javascript`: accepts a javascript code block, that must return a string value. The input string is declared in the `value` variable. If an error occurs while compiling or running the script, then the original value is returned.
Example:
```yaml
@@ -369,12 +369,11 @@ performer:
return value[0].toUpperCase() + value.substring(1)
}
```
We use [`goja` javascript engine](https://github.com/dop251/goja) which is missing a few built-in methods and may not be consistent with other modern javascript implementations.
Note that the `otto` javascript engine is missing a few built-in methods and may not be consistent with other modern javascript implementations.
* `feetToCm`: converts a string containing feet and inches numbers into centimeters. Looks for up to two separate integers and interprets the first as the number of feet, and the second as the number of inches. The numbers can be separated by any non-numeric character including the `.` character. It does not handle decimal numbers. For example `6.3` and `6ft3.3` would both be interpreted as 6 feet, 3 inches before converting into centimeters.
* `lbToKg`: converts a string containing lbs to kg.
* `map`: contains a map of input values to output values. Where a value matches one of the input values, it is replaced with the matching output value. If no value is matched, then value is unmodified.
Example:
```yaml
performer:
@@ -393,11 +392,8 @@ performer:
postProcess:
- lbToKg: true
```
Gets the contents of the selected div element, and sets the returned value to:
- `Female` if the scraped value is `F`;
- `Male` if the scraped value is `M`.
Height and weight are extracted from the selected spans and converted to `cm` and `kg`.
Gets the contents of the selected div element, and sets the returned value to `Female` if the scraped value is `F`; `Male` if the scraped value is `M`.
Height and weight are extracted from the selected spans and converted to `cm` and `kg`.
* `parseDate`: if present, the value is the date format using go's reference date (2006-01-02). For example, if an example date was `14-Mar-2003`, then the date format would be `02-Jan-2006`. See the [time.Parse documentation](https://golang.org/pkg/time/#Parse) for details. When present, the scraper will convert the input string into a date, then convert it to the string format used by stash (`YYYY-MM-DD`). Strings "Today", "Yesterday" are matched (case insensitive) and converted by the scraper so you don't need to edit/replace them.
Unix timestamps (example: 1660169451) can also be parsed by selecting `unix` as the date format.
@@ -422,6 +418,7 @@ Date:
```
* `replace`: contains an array of sub-objects. Each sub-object must have a `regex` and `with` field. The `regex` field is the regex pattern to replace, and `with` is the string to replace it with. `$` is used to reference capture groups - `$1` is the first capture group, `$2` the second and so on. Replacements are performed in order of the array.
Example:
```yaml
CareerLength:
@@ -436,9 +433,9 @@ Replaces `2001 to 2003` with `2001-2003`.
* `subScraper`: if present, the sub-scraper will be executed after all other post-processes are complete and before parseDate. It then takes the value and performs an http request, using the value as the URL. Within the `subScraper` config is a nested scraping configuration. This allows you to traverse to other webpages to get the attribute value you are after. For more info and examples have a look at [#370](https://github.com/stashapp/stash/pull/370), [#606](https://github.com/stashapp/stash/pull/606)
Additionally, there are a number of fixed post-processing fields that are specified at the attribute level (not in `postProcess`) that are performed after the `postProcess` operations:
* `concat`: if an xpath matches multiple elements, and `concat` is present, then all of the elements will be concatenated together
* `split`: the inverse of `concat`. Splits a string to more elements using the separator given. For more info and examples have a look at PR [#579](https://github.com/stashapp/stash/pull/579)
Example:
```yaml
Tags:
@@ -799,83 +796,120 @@ driver:
```
## Object fields
### Gallery
```
Code
Date
Details
Performers (see Performer fields)
Photographer
Rating
Studio (see Studio Fields)
Tags (see Tag fields)
Title
URLs
```
> **Important**: `Title` field is required.
### Group
```
Aliases
BackImage
Date
Director
Duration
FrontImage
Name
Rating
Studio (see Studio Fields)
Synopsis
Tags (see Tag fields)
URLs
```
> **Important**: `Name` field is required.
### Image
```
Code
Date
Details
Performers (see Performer fields)
Photographer
Rating
Studio (see Studio Fields)
Tags (see Tag fields)
Title
URLs
```
### Performer
```
Name
Gender
URL
Twitter
Instagram
Birthdate
DeathDate
Ethnicity
Country
HairColor
EyeColor
Height
Weight
Measurements
FakeTits
CareerLength
Tattoos
Piercings
Aliases
Tags (see Tag fields)
Image
Birthdate
CareerLength
Circumcised
Country
DeathDate
Details
Disambiguation
Ethnicity
EyeColor
FakeTits
Gender
HairColor
Height
Measurements
Name
PenisLength
Piercings
Tags (see Tag fields)
Tattoos
URLs
Weight
```
*Note:* - `Gender` must be one of `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary` (case insensitive).
> **Important**: `Name` field is required.
> **Note:** - `Gender` must be one of `male`, `female`, `transgender_male`, `transgender_female`, `intersex`, `non_binary` (case insensitive).
### Scene
```
Title
Details
Code
Director
URL
Date
Image
Studio (see Studio Fields)
Details
Director
Groups (see Group Fields)
Image
Performers (see Performer fields)
Studio (see Studio Fields)
Tags (see Tag fields)
Performers (list of Performer fields)
Title
URLs
```
> **Important**: `Title` field is required only if fileless.
### Studio
```
Name
URL
```
> **Important**: `Name` field is required.
### Tag
```
Name
```
### Group
```
Name
Aliases
Duration
Date
Rating
Director
Studio
Synopsis
URL
FrontImage
BackImage
```
### Gallery
```
Title
Details
URL
Date
Rating
Studio (see Studio Fields)
Tags (see Tag fields)
Performers (list of Performer fields)
```
> **Important**: `Name` field is required.

View File

@@ -981,11 +981,11 @@
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.8", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.27.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.0.tgz#fbee7cf97c709518ecc1f590984481d5460d4762"
integrity sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==
version "7.21.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
dependencies:
regenerator-runtime "^0.14.0"
regenerator-runtime "^0.13.11"
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.15", "@babel/template@^7.23.9":
version "7.23.9"
@@ -6726,11 +6726,6 @@ regenerator-runtime@^0.13.11:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
regenerator-transform@^0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56"
@@ -8028,10 +8023,10 @@ vite-tsconfig-paths@^4.0.5:
globrex "^0.1.2"
tsconfck "^2.0.1"
vite@^4.5.11:
version "4.5.11"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.11.tgz#796797f40d5bf5ce673d3773c9e0c4cbe204e85a"
integrity sha512-4mVdhLkZ0vpqZLGJhNm+X1n7juqXApEMGlUXcOQawA45UmpxivOYaMBkI/Js3FlBsNA8hCgEnX5X04moFitSGw==
vite@^4.5.6:
version "4.5.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.6.tgz#48bbd97fe06e8241df2e625b31c581707e10b57d"
integrity sha512-ElBNuVvJKslxcfY2gMmae5IjaKGqCYGicCNZ+8R56sAznobeE3pI9ctzI17cBS/6OJh5YuQNMSN4BP4dRjugBg==
dependencies:
esbuild "^0.18.10"
postcss "^8.4.27"