From 27ce5a4a920d325f157b2e3e8f65a4ce21b5823d Mon Sep 17 00:00:00 2001 From: Olive Vaughn Date: Wed, 26 Aug 2026 00:34:45 -0400 Subject: [PATCH] fuxxy serch + rebuild script --- deploy.sh | 14 +++++++++ internal/library/library.go | 60 +++++++++++++++++++++++++++++++++---- internal/scanner/scanner.go | 4 +++ 3 files changed, 73 insertions(+), 5 deletions(-) create mode 100755 deploy.sh diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..ab15566 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e + +REMOTE="root@minifridge" +REMOTE_DIR="/mnt/user/appdata/sheepflix" + +echo "syncing to $REMOTE:$REMOTE_DIR..." +rsync -avz --exclude '.git' --exclude 'testdata' --exclude 'sheepflix' \ + ./ "$REMOTE:$REMOTE_DIR/" + +echo "rebuilding on remote..." +ssh "$REMOTE" "cd $REMOTE_DIR && docker compose up -d --build" + +echo "done." diff --git a/internal/library/library.go b/internal/library/library.go index c27fcc9..ad29c01 100644 --- a/internal/library/library.go +++ b/internal/library/library.go @@ -134,17 +134,67 @@ func (l *Library) Get(id string) (*MediaItem, string, bool) { return item, item.fullPath, true } -// Search returns items whose name contains the query (case-insensitive). +// Search returns items matching the query, ranked by fuzzy match score. func (l *Library) Search(query string) []MediaItem { q := strings.ToLower(query) l.mu.RLock() defer l.mu.RUnlock() - var results []MediaItem + type scored struct { + item MediaItem + score int + } + var results []scored for _, item := range l.items { - if strings.Contains(strings.ToLower(item.Name), q) { - results = append(results, item) + name := strings.ToLower(item.Name) + if s := fuzzyScore(name, q); s > 0 { + results = append(results, scored{item, s}) } } - return results + sort.Slice(results, func(i, j int) bool { + return results[i].score > results[j].score + }) + + items := make([]MediaItem, len(results)) + for i, r := range results { + items[i] = r.item + } + return items +} + +// fuzzyScore returns a match score for how well target matches the query. +// Returns 0 if no match. Higher is better. +// Exact substring match gets a big bonus. Otherwise uses subsequence matching +// with bonuses for consecutive chars and word-boundary matches. +func fuzzyScore(target, query string) int { + // exact substring = best possible match + if strings.Contains(target, query) { + return 1000 + len(query)*10 + } + + // subsequence match with scoring + qi := 0 + score := 0 + consecutive := 0 + for ti := 0; ti < len(target) && qi < len(query); ti++ { + if target[ti] == query[qi] { + score += 10 + consecutive++ + if consecutive > 1 { + score += consecutive * 5 // bonus for runs + } + // word boundary bonus + if ti == 0 || target[ti-1] == ' ' || target[ti-1] == '.' || target[ti-1] == '_' || target[ti-1] == '-' { + score += 20 + } + qi++ + } else { + consecutive = 0 + } + } + + if qi < len(query) { + return 0 // didn't match all query chars + } + return score } diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index a2f91a0..d2e3a9d 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -45,6 +45,10 @@ func Scan(rootDir string) ([]MediaFile, error) { return nil } + if strings.HasPrefix(d.Name(), "._") { + return nil + } + ext := strings.ToLower(filepath.Ext(d.Name())) if !videoExtensions[ext] { return nil