diff --git a/pkg/api/context_keys.go b/pkg/api/context_keys.go index 839464af9..8731f75c3 100644 --- a/pkg/api/context_keys.go +++ b/pkg/api/context_keys.go @@ -5,8 +5,8 @@ package api type key int const ( - galleryKey key = iota - performerKey + // galleryKey key = 0 + performerKey key = iota + 1 sceneKey studioKey movieKey diff --git a/pkg/api/resolver_subscription_job.go b/pkg/api/resolver_subscription_job.go index c7b3176c0..2ee28df96 100644 --- a/pkg/api/resolver_subscription_job.go +++ b/pkg/api/resolver_subscription_job.go @@ -2,32 +2,12 @@ package api import ( "context" - "time" "github.com/stashapp/stash/pkg/job" "github.com/stashapp/stash/pkg/manager" "github.com/stashapp/stash/pkg/models" ) -type throttledUpdate struct { - id int - pendingUpdate *job.Job - lastUpdate time.Time - broadcastTimer *time.Timer - killTimer *time.Timer -} - -func (tu *throttledUpdate) broadcast(output chan *models.JobStatusUpdate) { - tu.lastUpdate = time.Now() - output <- &models.JobStatusUpdate{ - Type: models.JobStatusUpdateTypeUpdate, - Job: jobToJobModel(*tu.pendingUpdate), - } - - tu.broadcastTimer = nil - tu.pendingUpdate = nil -} - func makeJobStatusUpdate(t models.JobStatusUpdateType, j job.Job) *models.JobStatusUpdate { return &models.JobStatusUpdate{ Type: t, diff --git a/pkg/api/routes_scene.go b/pkg/api/routes_scene.go index d7bb1d888..0bc48f66f 100644 --- a/pkg/api/routes_scene.go +++ b/pkg/api/routes_scene.go @@ -16,8 +16,7 @@ import ( ) type sceneRoutes struct { - txnManager models.TransactionManager - sceneServer manager.SceneServer + txnManager models.TransactionManager } func (rs sceneRoutes) Routes() chi.Router { diff --git a/pkg/api/server.go b/pkg/api/server.go index 823dae457..cb969562c 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -98,16 +98,6 @@ func authenticateHandler() func(http.Handler) http.Handler { } } -func visitedPluginHandler() func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // get the visited plugins and set them in the context - - next.ServeHTTP(w, r) - }) - } -} - const loginEndPoint = "/login" func Start() { diff --git a/pkg/api/session.go b/pkg/api/session.go index 739df916f..cc14d0c28 100644 --- a/pkg/api/session.go +++ b/pkg/api/session.go @@ -10,11 +10,6 @@ import ( "github.com/stashapp/stash/pkg/session" ) -const cookieName = "session" -const usernameFormKey = "username" -const passwordFormKey = "password" -const userIDKey = "userID" - const returnURLParam = "returnURL" type loginTemplateData struct { diff --git a/pkg/dlna/dms.go b/pkg/dlna/dms.go index 85c25c979..599cc7820 100644 --- a/pkg/dlna/dms.go +++ b/pkg/dlna/dms.go @@ -58,7 +58,6 @@ const ( resPath = "/res" iconPath = "/icon" rootDescPath = "/rootDesc.xml" - contentDirectorySCPDURL = "/scpd/ContentDirectory.xml" contentDirectoryEventSubURL = "/evt/ContentDirectory" serviceControlURL = "/ctl" deviceIconPath = "/deviceIcon" diff --git a/pkg/ffmpeg/hls.go b/pkg/ffmpeg/hls.go index 4ac5788ec..f0f6b5205 100644 --- a/pkg/ffmpeg/hls.go +++ b/pkg/ffmpeg/hls.go @@ -21,9 +21,8 @@ func WriteHLSPlaylist(probeResult VideoFile, baseUrl string, w io.Writer) { leftover := duration upTo := 0.0 - tsURL := baseUrl i := strings.LastIndex(baseUrl, ".m3u8") - tsURL = baseUrl[0:i] + ".ts" + tsURL := baseUrl[0:i] + ".ts" for leftover > 0 { thisLength := hlsSegmentLength diff --git a/pkg/gallery/export_test.go b/pkg/gallery/export_test.go index 56b3e540a..2cd04fa17 100644 --- a/pkg/gallery/export_test.go +++ b/pkg/gallery/export_test.go @@ -19,7 +19,7 @@ const ( missingStudioID = 5 errStudioID = 6 - noTagsID = 11 + // noTagsID = 11 errTagsID = 12 ) @@ -39,11 +39,6 @@ const ( studioName = "studioName" ) -var names = []string{ - "name1", - "name2", -} - var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC) var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC) @@ -71,18 +66,6 @@ func createFullGallery(id int) models.Gallery { } } -func createEmptyGallery(id int) models.Gallery { - return models.Gallery{ - ID: id, - CreatedAt: models.SQLiteTimestamp{ - Timestamp: createTime, - }, - UpdatedAt: models.SQLiteTimestamp{ - Timestamp: updateTime, - }, - } -} - func createFullJSONGallery() *jsonschema.Gallery { return &jsonschema.Gallery{ Title: title, @@ -103,17 +86,6 @@ func createFullJSONGallery() *jsonschema.Gallery { } } -func createEmptyJSONGallery() *jsonschema.Gallery { - return &jsonschema.Gallery{ - CreatedAt: models.JSONTime{ - Time: createTime, - }, - UpdatedAt: models.JSONTime{ - Time: updateTime, - }, - } -} - type basicTestScenario struct { input models.Gallery expected *jsonschema.Gallery diff --git a/pkg/gallery/import_test.go b/pkg/gallery/import_test.go index 19de14f35..176c80810 100644 --- a/pkg/gallery/import_test.go +++ b/pkg/gallery/import_test.go @@ -13,8 +13,8 @@ import ( ) const ( - galleryNameErr = "galleryNameErr" - existingGalleryName = "existingGalleryName" + galleryNameErr = "galleryNameErr" + // existingGalleryName = "existingGalleryName" existingGalleryID = 100 existingStudioID = 101 diff --git a/pkg/image/export_test.go b/pkg/image/export_test.go index 9c7ab5ed1..25959aab0 100644 --- a/pkg/image/export_test.go +++ b/pkg/image/export_test.go @@ -13,8 +13,8 @@ import ( ) const ( - imageID = 1 - noImageID = 2 + imageID = 1 + // noImageID = 2 errImageID = 3 studioID = 4 @@ -24,17 +24,17 @@ const ( // noGalleryID = 7 // errGalleryID = 8 - noTagsID = 11 + // noTagsID = 11 errTagsID = 12 - noMoviesID = 13 - errMoviesID = 14 - errFindMovieID = 15 + // noMoviesID = 13 + // errMoviesID = 14 + // errFindMovieID = 15 - noMarkersID = 16 - errMarkersID = 17 - errFindPrimaryTagID = 18 - errFindByMarkerID = 19 + // noMarkersID = 16 + // errMarkersID = 17 + // errFindPrimaryTagID = 18 + // errFindByMarkerID = 19 ) const ( @@ -53,11 +53,6 @@ const ( //galleryChecksum = "galleryChecksum" ) -var names = []string{ - "name1", - "name2", -} - var createTime time.Time = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC) var updateTime time.Time = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC) @@ -81,18 +76,6 @@ func createFullImage(id int) models.Image { } } -func createEmptyImage(id int) models.Image { - return models.Image{ - ID: id, - CreatedAt: models.SQLiteTimestamp{ - Timestamp: createTime, - }, - UpdatedAt: models.SQLiteTimestamp{ - Timestamp: updateTime, - }, - } -} - func createFullJSONImage() *jsonschema.Image { return &jsonschema.Image{ Title: title, @@ -114,18 +97,6 @@ func createFullJSONImage() *jsonschema.Image { } } -func createEmptyJSONImage() *jsonschema.Image { - return &jsonschema.Image{ - File: &jsonschema.ImageFile{}, - CreatedAt: models.JSONTime{ - Time: createTime, - }, - UpdatedAt: models.JSONTime{ - Time: updateTime, - }, - } -} - type basicTestScenario struct { input models.Image expected *jsonschema.Image diff --git a/pkg/image/import_test.go b/pkg/image/import_test.go index 91978dac8..8b7099823 100644 --- a/pkg/image/import_test.go +++ b/pkg/image/import_test.go @@ -11,20 +11,18 @@ import ( "github.com/stretchr/testify/mock" ) -const invalidImage = "aW1hZ2VCeXRlcw&&" - const ( path = "path" - imageNameErr = "imageNameErr" - existingImageName = "existingImageName" + imageNameErr = "imageNameErr" + // existingImageName = "existingImageName" existingImageID = 100 existingStudioID = 101 existingGalleryID = 102 existingPerformerID = 103 - existingMovieID = 104 - existingTagID = 105 + // existingMovieID = 104 + existingTagID = 105 existingStudioName = "existingStudioName" existingStudioErr = "existingStudioErr" diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 8e2de2bbf..fde7e09c2 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -41,7 +41,6 @@ func Init(logFile string, logOut bool, logLevel string) { if err != nil { fmt.Printf("Could not open '%s' for log output due to error: %s\n", logFile, err.Error()) - logFile = "" } } diff --git a/pkg/manager/filename_parser.go b/pkg/manager/filename_parser.go index 991261941..6373c4a2a 100644 --- a/pkg/manager/filename_parser.go +++ b/pkg/manager/filename_parser.go @@ -11,8 +11,6 @@ import ( "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/tag" - - "github.com/jmoiron/sqlx" ) type parserField struct { @@ -52,10 +50,6 @@ func (f parserField) replaceInPattern(pattern string) string { return string(f.fieldRegex.ReplaceAllString(pattern, f.regex)) } -func (f parserField) getFieldPattern() string { - return "{" + f.field + "}" -} - var validFields map[string]parserField var escapeCharRE *regexp.Regexp var capitalizeTitleRE *regexp.Regexp @@ -405,26 +399,6 @@ func (m parseMapper) parse(scene *models.Scene) *sceneHolder { return sceneHolder } -type performerQueryer interface { - FindByNames(names []string, tx *sqlx.Tx, nocase bool) ([]*models.Performer, error) -} - -type sceneQueryer interface { - QueryByPathRegex(findFilter *models.FindFilterType) ([]*models.Scene, int) -} - -type tagQueryer interface { - FindByName(name string, tx *sqlx.Tx, nocase bool) (*models.Tag, error) -} - -type studioQueryer interface { - FindByName(name string, tx *sqlx.Tx, nocase bool) (*models.Studio, error) -} - -type movieQueryer interface { - FindByName(name string, tx *sqlx.Tx, nocase bool) (*models.Movie, error) -} - type SceneFilenameParser struct { Pattern string ParserInput models.SceneParserInput diff --git a/pkg/manager/jsonschema/utils.go b/pkg/manager/jsonschema/utils.go index 2844026f7..13f42c820 100644 --- a/pkg/manager/jsonschema/utils.go +++ b/pkg/manager/jsonschema/utils.go @@ -4,13 +4,10 @@ import ( "bytes" "io/ioutil" - "time" jsoniter "github.com/json-iterator/go" ) -var nilTime = (time.Time{}).UnixNano() - func CompareJSON(a interface{}, b interface{}) bool { aBuf, _ := encode(a) bBuf, _ := encode(b) diff --git a/pkg/manager/task_import.go b/pkg/manager/task_import.go index 84acaaeef..db3d9f76d 100644 --- a/pkg/manager/task_import.go +++ b/pkg/manager/task_import.go @@ -569,101 +569,6 @@ func (t *ImportTask) ImportImages(ctx context.Context) { logger.Info("[images] import complete") } -func (t *ImportTask) getPerformers(names []string, qb models.PerformerReader) ([]*models.Performer, error) { - performers, err := qb.FindByNames(names, false) - if err != nil { - return nil, err - } - - var pluckedNames []string - for _, performer := range performers { - if !performer.Name.Valid { - continue - } - pluckedNames = append(pluckedNames, performer.Name.String) - } - - missingPerformers := utils.StrFilter(names, func(name string) bool { - return !utils.StrInclude(pluckedNames, name) - }) - - for _, missingPerformer := range missingPerformers { - logger.Warnf("[scenes] performer %s does not exist", missingPerformer) - } - - return performers, nil -} - -func (t *ImportTask) getMoviesScenes(input []jsonschema.SceneMovie, sceneID int, mqb models.MovieReader) ([]models.MoviesScenes, error) { - var movies []models.MoviesScenes - for _, inputMovie := range input { - movie, err := mqb.FindByName(inputMovie.MovieName, false) - if err != nil { - return nil, err - } - - if movie == nil { - logger.Warnf("[scenes] movie %s does not exist", inputMovie.MovieName) - } else { - toAdd := models.MoviesScenes{ - MovieID: movie.ID, - SceneID: sceneID, - } - - if inputMovie.SceneIndex != 0 { - toAdd.SceneIndex = sql.NullInt64{ - Int64: int64(inputMovie.SceneIndex), - Valid: true, - } - } - - movies = append(movies, toAdd) - } - } - - return movies, nil -} - -func (t *ImportTask) getTags(sceneChecksum string, names []string, tqb models.TagReader) ([]*models.Tag, error) { - tags, err := tqb.FindByNames(names, false) - if err != nil { - return nil, err - } - - var pluckedNames []string - for _, tag := range tags { - if tag.Name == "" { - continue - } - pluckedNames = append(pluckedNames, tag.Name) - } - - missingTags := utils.StrFilter(names, func(name string) bool { - return !utils.StrInclude(pluckedNames, name) - }) - - for _, missingTag := range missingTags { - logger.Warnf("[scenes] <%s> tag %s does not exist", sceneChecksum, missingTag) - } - - return tags, nil -} - -// https://www.reddit.com/r/golang/comments/5ia523/idiomatic_way_to_remove_duplicates_in_a_slice/db6qa2e -func (t *ImportTask) getUnique(s []string) []string { - seen := make(map[string]struct{}, len(s)) - j := 0 - for _, v := range s { - if _, ok := seen[v]; ok { - continue - } - seen[v] = struct{}{} - s[j] = v - j++ - } - return s[:j] -} - var currentLocation = time.Now().Location() func (t *ImportTask) getTimeFromJSONTime(jsonTime models.JSONTime) time.Time { diff --git a/pkg/scene/export_test.go b/pkg/scene/export_test.go index dc3164f13..422008fa2 100644 --- a/pkg/scene/export_test.go +++ b/pkg/scene/export_test.go @@ -23,8 +23,8 @@ const ( missingStudioID = 5 errStudioID = 6 - noGalleryID = 7 - errGalleryID = 8 + // noGalleryID = 7 + // errGalleryID = 8 noTagsID = 11 errTagsID = 12 @@ -64,8 +64,8 @@ const ( ) const ( - studioName = "studioName" - galleryChecksum = "galleryChecksum" + studioName = "studioName" + // galleryChecksum = "galleryChecksum" validMovie1 = 1 validMovie2 = 2 @@ -293,24 +293,6 @@ func TestGetStudioName(t *testing.T) { mockStudioReader.AssertExpectations(t) } -var getGalleryChecksumScenarios = []stringTestScenario{ - { - createEmptyScene(sceneID), - galleryChecksum, - false, - }, - { - createEmptyScene(noGalleryID), - "", - false, - }, - { - createEmptyScene(errGalleryID), - "", - true, - }, -} - type stringSliceTestScenario struct { input models.Scene expected []string diff --git a/pkg/scene/import_test.go b/pkg/scene/import_test.go index 09f0bf38d..3f59c3cf3 100644 --- a/pkg/scene/import_test.go +++ b/pkg/scene/import_test.go @@ -16,8 +16,8 @@ const invalidImage = "aW1hZ2VCeXRlcw&&" const ( path = "path" - sceneNameErr = "sceneNameErr" - existingSceneName = "existingSceneName" + sceneNameErr = "sceneNameErr" + // existingSceneName = "existingSceneName" existingSceneID = 100 existingStudioID = 101 diff --git a/pkg/scraper/action.go b/pkg/scraper/action.go index fdfa15afa..ac5e163b6 100644 --- a/pkg/scraper/action.go +++ b/pkg/scraper/action.go @@ -11,13 +11,6 @@ const ( scraperActionJson scraperAction = "scrapeJson" ) -var allScraperAction = []scraperAction{ - scraperActionScript, - scraperActionStash, - scraperActionXPath, - scraperActionJson, -} - func (e scraperAction) IsValid() bool { switch e { case scraperActionScript, scraperActionStash, scraperActionXPath, scraperActionJson: @@ -26,12 +19,6 @@ func (e scraperAction) IsValid() bool { return false } -type scrapeOptions struct { - scraper scraperTypeConfig - config config - globalConfig GlobalConfig -} - type scraper interface { scrapePerformersByName(name string) ([]*models.ScrapedPerformer, error) scrapePerformerByFragment(scrapedPerformer models.ScrapedPerformerInput) (*models.ScrapedPerformer, error) diff --git a/pkg/scraper/mapped.go b/pkg/scraper/mapped.go index 52af8556c..8a8e2b847 100644 --- a/pkg/scraper/mapped.go +++ b/pkg/scraper/mapped.go @@ -557,7 +557,7 @@ func (a mappedPostProcessAction) ToPostProcessAction() (postProcessAction, error if found != "" { return nil, fmt.Errorf("post-process actions must have a single field, found %s and %s", found, "subtractDays") } - found = "subtractDays" + // found = "subtractDays" action := postProcessSubtractDays(a.SubtractDays) ret = &action } diff --git a/pkg/scraper/url.go b/pkg/scraper/url.go index e2af1cb97..d2adaa81b 100644 --- a/pkg/scraper/url.go +++ b/pkg/scraper/url.go @@ -235,17 +235,6 @@ func getRemoteCDPWSAddress(address string) (string, error) { return remote, err } -func cdpNetwork(enable bool) chromedp.Action { - return chromedp.ActionFunc(func(ctx context.Context) error { - if enable { - network.Enable().Do(ctx) - } else { - network.Disable().Do(ctx) - } - return nil - }) -} - func cdpHeaders(driverOptions scraperDriverOptions) map[string]interface{} { headers := map[string]interface{}{} if driverOptions.Headers != nil { diff --git a/pkg/session/session.go b/pkg/session/session.go index 55faa4282..9d754e63a 100644 --- a/pkg/session/session.go +++ b/pkg/session/session.go @@ -173,19 +173,6 @@ func setVisitedPlugins(ctx context.Context, visitedPlugins []string) context.Con return context.WithValue(ctx, contextVisitedPlugins, visitedPlugins) } -func (s *Store) createSessionCookie(username string) (*http.Cookie, error) { - session := sessions.NewSession(s.sessionStore, cookieName) - session.Values[userIDKey] = username - - encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, - s.sessionStore.Codecs...) - if err != nil { - return nil, err - } - - return sessions.NewCookie(session.Name(), encoded, session.Options), nil -} - func (s *Store) MakePluginCookie(ctx context.Context) *http.Cookie { currentUser := GetCurrentUserID(ctx) visitedPlugins := GetVisitedPlugins(ctx) diff --git a/pkg/sqlite/filter.go b/pkg/sqlite/filter.go index 1517bf99a..d83ce12b0 100644 --- a/pkg/sqlite/filter.go +++ b/pkg/sqlite/filter.go @@ -389,14 +389,6 @@ func boolCriterionHandler(c *bool, column string) criterionHandlerFunc { } } -func stringLiteralCriterionHandler(v *string, column string) criterionHandlerFunc { - return func(f *filterBuilder) { - if v != nil { - f.addWhere(column+" = ?", v) - } - } -} - // handle for MultiCriterion where there is a join table between the new // objects type joinedMultiCriterionHandlerBuilder struct { diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index 66bf2032a..30f13c75c 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -311,17 +311,6 @@ func galleryIsMissingCriterionHandler(qb *galleryQueryBuilder, isMissing *string } } -func (qb *galleryQueryBuilder) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder { - return multiCriterionHandlerBuilder{ - primaryTable: galleryTable, - foreignTable: foreignTable, - joinTable: joinTable, - primaryFK: galleryIDColumn, - foreignFK: foreignFK, - addJoinsFunc: addJoinsFunc, - } -} - func galleryTagsCriterionHandler(qb *galleryQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc { h := joinedMultiCriterionHandlerBuilder{ primaryTable: galleryTable, diff --git a/pkg/sqlite/scraped_item.go b/pkg/sqlite/scraped_item.go index 39e040f89..30f772dc9 100644 --- a/pkg/sqlite/scraped_item.go +++ b/pkg/sqlite/scraped_item.go @@ -72,14 +72,6 @@ func (qb *scrapedItemQueryBuilder) getScrapedItemsSort(findFilter *models.FindFi return getSort(sort, direction, "scraped_items") } -func (qb *scrapedItemQueryBuilder) queryScrapedItem(query string, args []interface{}) (*models.ScrapedItem, error) { - results, err := qb.queryScrapedItems(query, args) - if err != nil || len(results) < 1 { - return nil, err - } - return results[0], nil -} - func (qb *scrapedItemQueryBuilder) queryScrapedItems(query string, args []interface{}) ([]*models.ScrapedItem, error) { var ret models.ScrapedItems if err := qb.query(query, args, &ret); err != nil { diff --git a/pkg/sqlite/sql.go b/pkg/sqlite/sql.go index 7c9477bdc..7101fe489 100644 --- a/pkg/sqlite/sql.go +++ b/pkg/sqlite/sql.go @@ -8,7 +8,6 @@ import ( "strconv" "strings" - "github.com/jmoiron/sqlx" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/models" ) @@ -148,32 +147,6 @@ func getInBinding(length int) string { return "(" + bindings + ")" } -func getCriterionModifierBinding(criterionModifier models.CriterionModifier, value interface{}) (string, int) { - var length int - switch x := value.(type) { - case []string: - length = len(x) - case []int: - length = len(x) - default: - length = 1 - } - if modifier := criterionModifier.String(); criterionModifier.IsValid() { - switch modifier { - case "EQUALS", "NOT_EQUALS", "GREATER_THAN", "LESS_THAN", "IS_NULL", "NOT_NULL", "BETWEEN", "NOT_BETWEEN": - return getSimpleCriterionClause(criterionModifier, "?") - case "INCLUDES": - return "IN " + getInBinding(length), length // TODO? - case "EXCLUDES": - return "NOT IN " + getInBinding(length), length // TODO? - default: - logger.Errorf("todo") - return "= ?", 1 // TODO - } - } - return "= ?", 1 // TODO -} - func getSimpleCriterionClause(criterionModifier models.CriterionModifier, rhs string) (string, int) { if modifier := criterionModifier.String(); criterionModifier.IsValid() { switch modifier { @@ -252,12 +225,6 @@ func getCountCriterionClause(primaryTable, joinTable, primaryFK string, criterio return getIntCriterionWhereClause(lhs, criterion) } -func ensureTx(tx *sqlx.Tx) { - if tx == nil { - panic("must use a transaction") - } -} - func getImage(tx dbi, query string, args ...interface{}) ([]byte, error) { rows, err := tx.Queryx(query, args...) diff --git a/pkg/utils/byterange.go b/pkg/utils/byterange.go index 2220b8dc4..5fb031ea7 100644 --- a/pkg/utils/byterange.go +++ b/pkg/utils/byterange.go @@ -30,14 +30,6 @@ func CreateByteRange(s string) ByteRange { return ret } -func (r ByteRange) getBytesToRead() int64 { - if r.End == nil { - return 0 - } - - return *r.End - r.Start + 1 -} - func (r ByteRange) ToHeaderValue(fileLength int64) string { if r.End == nil { return ""