Release v0.2.34
This commit is contained in:
@@ -522,6 +522,12 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request, sess store
|
||||
limit := queryInt(r, "limit", 40, 100)
|
||||
key := cache.UserKey(sess.EmbyUserID, "search:"+itoa(limit)+":"+term+":d"+sess.DeviceID)
|
||||
|
||||
// Every search the tab performs is recorded here, before the cache is consulted, so a
|
||||
// query answered from Redis counts the same as one that reached Emby. The client also
|
||||
// posts to /v1/search/history and an older APK is the only thing that records at all —
|
||||
// recordSearchQuery's dedupe window is what stops the two writing the same query twice.
|
||||
s.recordSearchQuery(ctx, sess, term)
|
||||
|
||||
if raw, err := s.cache.Get(ctx, key); err == nil {
|
||||
w.Header().Set("X-Memby-Cache", "hit")
|
||||
writeRaw(w, http.StatusOK, raw)
|
||||
@@ -560,6 +566,49 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request, sess store
|
||||
writeRaw(w, http.StatusOK, body)
|
||||
}
|
||||
|
||||
const (
|
||||
// minSearchQueryRunes matches the client's own floor: one letter matches half a
|
||||
// library, so the search tab does not ask below two and neither route records below it.
|
||||
minSearchQueryRunes = 2
|
||||
// maxSearchQueryRunes bounds what is written to search_history. The query arrives in a
|
||||
// URL on one of the two routes, so the table's row size must not be the client's to
|
||||
// choose. Runes rather than bytes, or a title in Japanese is rejected at a third of the
|
||||
// length of one in English.
|
||||
maxSearchQueryRunes = 200
|
||||
)
|
||||
|
||||
// searchQueryRecordable is the one rule both routes apply, so a query the search handler
|
||||
// records is exactly one the history endpoint would have accepted.
|
||||
func searchQueryRecordable(term string) bool {
|
||||
n := len([]rune(strings.TrimSpace(term)))
|
||||
return n >= minSearchQueryRunes && n <= maxSearchQueryRunes
|
||||
}
|
||||
|
||||
// recordSearchQuery writes a query the search tab performed, and never makes the viewer
|
||||
// wait for it.
|
||||
//
|
||||
// Detached from the request context deliberately: instant search cancels the in-flight
|
||||
// request on every keystroke (the client's collectLatest), so a write hung off r.Context()
|
||||
// would be abandoned for precisely the searches somebody typed fastest — and the record is
|
||||
// worth having whether or not they waited for the results.
|
||||
func (s *Server) recordSearchQuery(ctx context.Context, sess store.Session, term string) {
|
||||
if s.store == nil || !searchQueryRecordable(term) {
|
||||
return
|
||||
}
|
||||
term = strings.TrimSpace(term)
|
||||
log := s.loggerFor(ctx)
|
||||
detached := context.WithoutCancel(ctx)
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(detached, 5*time.Second)
|
||||
defer cancel()
|
||||
if err := s.store.RecordSearch(ctx, sess.EmbyUserID, term); err != nil {
|
||||
// Telemetry, not the answer: a search whose record failed still returns
|
||||
// results, and this is DEBUG for the same reason the search line itself is.
|
||||
log.Debug("search not recorded", "query", term, "error", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
type searchHistoryRequest struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
@@ -602,7 +651,7 @@ func (s *Server) handleSearchHistory(w http.ResponseWriter, r *http.Request, ses
|
||||
return
|
||||
}
|
||||
query := strings.TrimSpace(req.Query)
|
||||
if len([]rune(query)) < 2 || len([]rune(query)) > 200 {
|
||||
if !searchQueryRecordable(query) {
|
||||
writeError(w, http.StatusBadRequest, "search query length is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user