Added fuzzy finder to search, made sync check as middleware, .id file is never used
Build / build (push) Successful in 45s
Build / build (push) Successful in 45s
This commit is contained in:
@@ -1,117 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"music-server/internal/backend"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var soundtracks_added []string
|
|
||||||
|
|
||||||
func FindSoundtrackWebHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
err := r.ParseForm()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
search_term := r.FormValue("search_term")
|
|
||||||
|
|
||||||
search(search_term)
|
|
||||||
|
|
||||||
component := FoundSoundtracks(soundtracks_added)
|
|
||||||
err = component.Render(r.Context(), w)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
log.Fatalf("Error rendering in FindSoundtrackWebHandler: %e", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func search(searchText string) {
|
|
||||||
soundtracks_added = nil
|
|
||||||
soundtracks := backend.GetAllSoundtracks()
|
|
||||||
for _, soundtrack := range soundtracks {
|
|
||||||
if is_match_exact(searchText, soundtrack) {
|
|
||||||
add_soundtrack(soundtrack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, soundtrack := range soundtracks {
|
|
||||||
if is_match_contains(clean_term(searchText), clean_term(soundtrack)) {
|
|
||||||
add_soundtrack(soundtrack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, soundtrack := range soundtracks {
|
|
||||||
if is_match_regex(clean_term(searchText), clean_term(soundtrack)) {
|
|
||||||
add_soundtrack(soundtrack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func is_match_exact(search_term string, soundtrack_name string) bool {
|
|
||||||
search_term = strings.ToLower(search_term)
|
|
||||||
soundtrack_name = strings.ToLower(soundtrack_name)
|
|
||||||
|
|
||||||
if search_term == "" {
|
|
||||||
return true
|
|
||||||
} else if strings.Contains(soundtrack_name, search_term) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func is_match_contains(search_term string, soundtrack_name string) bool {
|
|
||||||
if search_term == "" {
|
|
||||||
return true
|
|
||||||
} else if strings.Contains(soundtrack_name, search_term) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func is_match_regex(search_term string, soundtrack_name string) bool {
|
|
||||||
if search_term == "" {
|
|
||||||
return true
|
|
||||||
} else if compile_regex(search_term).MatchString(soundtrack_name) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func add_soundtrack(soundtrack string) {
|
|
||||||
if !check_if_soundtrack_exists(soundtrack) {
|
|
||||||
soundtracks_added = append(soundtracks_added, soundtrack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func check_if_soundtrack_exists(soundtrackName string) bool {
|
|
||||||
soundtrack_exists := false
|
|
||||||
for _, child := range soundtracks_added {
|
|
||||||
if child == soundtrackName {
|
|
||||||
soundtrack_exists = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return soundtrack_exists
|
|
||||||
}
|
|
||||||
|
|
||||||
func compile_regex(search_term string) *regexp.Regexp {
|
|
||||||
regText := ".*"
|
|
||||||
for _, letter := range search_term {
|
|
||||||
regText += string(letter) + ".*"
|
|
||||||
}
|
|
||||||
r, _ := regexp.Compile(regText)
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func clean_term(term string) string {
|
|
||||||
term = strings.ReplaceAll(term, " ", "")
|
|
||||||
term = strings.ReplaceAll(term, "é", "e")
|
|
||||||
term = strings.ReplaceAll(term, "+", "plus")
|
|
||||||
term = strings.ReplaceAll(term, "&", "and")
|
|
||||||
term = strings.ReplaceAll(term, "'n", "and")
|
|
||||||
return strings.ToLower(term)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"music-server/internal/backend"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
var soundtracks_added []string
|
||||||
|
|
||||||
|
// Precomputed data for optimization
|
||||||
|
type SoundtrackData struct {
|
||||||
|
Original string
|
||||||
|
Cleaned string
|
||||||
|
Abbreviation string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
precomputedSoundtracks []SoundtrackData
|
||||||
|
precomputedOnce sync.Once
|
||||||
|
regexCache = make(map[string]*regexp.Regexp)
|
||||||
|
regexCacheMutex sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindSoundtrackWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
search_term := r.FormValue("search_term")
|
||||||
|
|
||||||
|
search(search_term)
|
||||||
|
|
||||||
|
component := FoundSoundtracks(soundtracks_added)
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
log.Fatalf("Error rendering in FindSoundtrackWebHandler: %e", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func search(searchText string) {
|
||||||
|
soundtracks_added = nil
|
||||||
|
|
||||||
|
// Precompute cleaned search term once
|
||||||
|
cleanedSearchTerm := clean_term(searchText)
|
||||||
|
|
||||||
|
// Use precomputed data for efficiency
|
||||||
|
soundtrackData := getPrecomputedSoundtracks()
|
||||||
|
seen := make(map[string]bool) // O(1) duplicate checking
|
||||||
|
|
||||||
|
for _, data := range soundtrackData {
|
||||||
|
// Check exact match (case-insensitive contains)
|
||||||
|
if is_match_exact(searchText, data.Original) && !seen[data.Original] {
|
||||||
|
soundtracks_added = append(soundtracks_added, data.Original)
|
||||||
|
seen[data.Original] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range soundtrackData {
|
||||||
|
// Check contains match with cleaned terms
|
||||||
|
if !seen[data.Original] && is_match_contains(cleanedSearchTerm, data.Cleaned) {
|
||||||
|
soundtracks_added = append(soundtracks_added, data.Original)
|
||||||
|
seen[data.Original] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range soundtrackData {
|
||||||
|
// Check regex match with cleaned terms
|
||||||
|
if !seen[data.Original] && is_match_regex_cached(cleanedSearchTerm, data.Cleaned) {
|
||||||
|
soundtracks_added = append(soundtracks_added, data.Original)
|
||||||
|
seen[data.Original] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_match_exact(search_term string, soundtrack_name string) bool {
|
||||||
|
search_term = strings.ToLower(search_term)
|
||||||
|
soundtrack_name = strings.ToLower(soundtrack_name)
|
||||||
|
|
||||||
|
if search_term == "" {
|
||||||
|
return true
|
||||||
|
} else if strings.Contains(soundtrack_name, search_term) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_match_contains(search_term string, soundtrack_name string) bool {
|
||||||
|
if search_term == "" {
|
||||||
|
return true
|
||||||
|
} else if strings.Contains(soundtrack_name, search_term) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_match_regex(search_term string, soundtrack_name string) bool {
|
||||||
|
if search_term == "" {
|
||||||
|
return true
|
||||||
|
} else if compile_regex(search_term).MatchString(soundtrack_name) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compile_regex(search_term string) *regexp.Regexp {
|
||||||
|
regText := ".*"
|
||||||
|
for _, letter := range search_term {
|
||||||
|
regText += string(letter) + ".*"
|
||||||
|
}
|
||||||
|
r, _ := regexp.Compile(regText)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// is_match_regex_cached uses cached compiled regex patterns for better performance
|
||||||
|
func is_match_regex_cached(search_term string, soundtrack_name string) bool {
|
||||||
|
if search_term == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
regexCacheMutex.Lock()
|
||||||
|
re, exists := regexCache[search_term]
|
||||||
|
if !exists {
|
||||||
|
re = compile_regex(search_term)
|
||||||
|
regexCache[search_term] = re
|
||||||
|
}
|
||||||
|
regexCacheMutex.Unlock()
|
||||||
|
|
||||||
|
return re.MatchString(soundtrack_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clean_term(term string) string {
|
||||||
|
term = strings.ReplaceAll(term, " ", "")
|
||||||
|
term = strings.ReplaceAll(term, "é", "e")
|
||||||
|
term = strings.ReplaceAll(term, "+", "plus")
|
||||||
|
term = strings.ReplaceAll(term, "&", "and")
|
||||||
|
term = strings.ReplaceAll(term, "'n", "and")
|
||||||
|
return strings.ToLower(term)
|
||||||
|
}
|
||||||
|
|
||||||
|
// toLower converts a string to lowercase.
|
||||||
|
func toLower(s string) string {
|
||||||
|
return strings.Map(unicode.ToLower, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// levenshteinWithThreshold calculates Levenshtein distance with early termination and space optimization.
|
||||||
|
// Uses O(min(n,m)) space instead of O(n*m) and stops early if threshold is exceeded.
|
||||||
|
func levenshteinWithThreshold(s, t string, threshold int) int {
|
||||||
|
if threshold < 0 {
|
||||||
|
threshold = 2 // default threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
m, n := len(s), len(t)
|
||||||
|
|
||||||
|
// Quick checks for early termination
|
||||||
|
if m == 0 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
if abs(m-n) > threshold {
|
||||||
|
return threshold + 1 // Can't be within threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the shorter string for the row to minimize space
|
||||||
|
if m < n {
|
||||||
|
s, t = t, s
|
||||||
|
m, n = n, m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Space optimization: only store two rows
|
||||||
|
prevRow := make([]int, n+1)
|
||||||
|
currRow := make([]int, n+1)
|
||||||
|
|
||||||
|
// Initialize first row
|
||||||
|
for j := 0; j <= n; j++ {
|
||||||
|
prevRow[j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= m; i++ {
|
||||||
|
currRow[0] = i
|
||||||
|
minInRow := currRow[0]
|
||||||
|
|
||||||
|
for j := 1; j <= n; j++ {
|
||||||
|
if s[i-1] == t[j-1] {
|
||||||
|
currRow[j] = prevRow[j-1]
|
||||||
|
} else {
|
||||||
|
currRow[j] = min3(prevRow[j], currRow[j-1], prevRow[j-1]) + 1
|
||||||
|
}
|
||||||
|
if currRow[j] < minInRow {
|
||||||
|
minInRow = currRow[j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Early termination: if minimum in current row exceeds threshold
|
||||||
|
if minInRow > threshold {
|
||||||
|
return threshold + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap rows for next iteration
|
||||||
|
prevRow, currRow = currRow, prevRow
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevRow[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions for optimized Levenshtein
|
||||||
|
func abs(x int) int {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func min3(a, b, c int) int {
|
||||||
|
if a < b {
|
||||||
|
if a < c {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if b < c {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// precomputeSoundtrackData initializes cleaned terms and abbreviations for all soundtracks
|
||||||
|
func precomputeSoundtrackData() {
|
||||||
|
precomputedOnce.Do(func() {
|
||||||
|
soundtracks := backend.GetAllSoundtracks()
|
||||||
|
precomputedSoundtracks = make([]SoundtrackData, len(soundtracks))
|
||||||
|
|
||||||
|
for i, soundtrack := range soundtracks {
|
||||||
|
cleaned := clean_term(soundtrack)
|
||||||
|
precomputedSoundtracks[i] = SoundtrackData{
|
||||||
|
Original: soundtrack,
|
||||||
|
Cleaned: cleaned,
|
||||||
|
Abbreviation: extractAbbreviation(soundtrack),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPrecomputedSoundtracks ensures precomputed data is available and returns it
|
||||||
|
func getPrecomputedSoundtracks() []SoundtrackData {
|
||||||
|
precomputeSoundtrackData()
|
||||||
|
return precomputedSoundtracks
|
||||||
|
}
|
||||||
|
|
||||||
|
// levenshtein maintains backward compatibility by calling the optimized version
|
||||||
|
func levenshtein(s, t string) int {
|
||||||
|
return levenshteinWithThreshold(s, t, -1) // -1 uses default threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractAbbreviation extracts the first letters of each word in a string.
|
||||||
|
func extractAbbreviation(s string) string {
|
||||||
|
words := strings.Fields(s)
|
||||||
|
abbr := ""
|
||||||
|
for _, word := range words {
|
||||||
|
if len(word) > 0 {
|
||||||
|
abbr += string(word[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toLower(abbr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzzyFindSubstring checks if the query fuzzy-matches any substring of the item.
|
||||||
|
func fuzzyFindSubstring(query, item string, threshold int) (bool, int) {
|
||||||
|
query = toLower(query)
|
||||||
|
item = toLower(item)
|
||||||
|
queryLen := len(query)
|
||||||
|
itemLen := len(item)
|
||||||
|
if queryLen > itemLen {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for exact substring match first (weight: 100)
|
||||||
|
if strings.Contains(item, query) {
|
||||||
|
return true, 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for fuzzy substring match using optimized Levenshtein
|
||||||
|
for i := 0; i <= itemLen-queryLen; i++ {
|
||||||
|
substring := item[i : i+queryLen]
|
||||||
|
distance := levenshteinWithThreshold(query, substring, threshold)
|
||||||
|
if distance <= threshold {
|
||||||
|
// Weight: higher for matches at the start of the string
|
||||||
|
weight := 50 - i // Higher weight for earlier matches
|
||||||
|
return true, weight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzzyFind checks if the query matches the item (substring or abbreviation).
|
||||||
|
func fuzzyFind(query string, item string, threshold int) (bool, int) {
|
||||||
|
query = toLower(query)
|
||||||
|
item = toLower(item)
|
||||||
|
|
||||||
|
// Check for substring fuzzy match
|
||||||
|
if matched, weight := fuzzyFindSubstring(query, item, threshold); matched {
|
||||||
|
return true, weight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for abbreviation match using optimized Levenshtein
|
||||||
|
abbr := extractAbbreviation(item)
|
||||||
|
distance := levenshteinWithThreshold(query, abbr, threshold)
|
||||||
|
if distance <= threshold {
|
||||||
|
// Weight: higher for exact abbreviation matches
|
||||||
|
weight := 100 - distance*10 // Higher weight for exact matches
|
||||||
|
return true, weight
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAdaptiveThreshold returns a threshold based on query length
|
||||||
|
func getAdaptiveThreshold(query string) int {
|
||||||
|
threshold := 2 // Base threshold for minor typos
|
||||||
|
queryLen := len(query)
|
||||||
|
if queryLen > 6 {
|
||||||
|
// For longer queries, allow more tolerance
|
||||||
|
threshold = queryLen / 3
|
||||||
|
if threshold < 2 {
|
||||||
|
threshold = 2
|
||||||
|
} else if threshold > 4 {
|
||||||
|
threshold = 4 // Cap at 4 for very long queries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzzySearch performs fuzzy search on soundtracks using the cleaned terms for consistency
|
||||||
|
func fuzzySearch(searchText string) []string {
|
||||||
|
query := clean_term(searchText)
|
||||||
|
threshold := getAdaptiveThreshold(query)
|
||||||
|
|
||||||
|
// Use precomputed data for efficiency
|
||||||
|
soundtrackData := getPrecomputedSoundtracks()
|
||||||
|
|
||||||
|
type match struct {
|
||||||
|
item string
|
||||||
|
weight int
|
||||||
|
}
|
||||||
|
var matches []match
|
||||||
|
seen := make(map[string]bool) // O(1) duplicate checking
|
||||||
|
|
||||||
|
for _, data := range soundtrackData {
|
||||||
|
if matched, weight := fuzzyFind(query, data.Cleaned, threshold); matched {
|
||||||
|
if !seen[data.Original] {
|
||||||
|
matches = append(matches, match{data.Original, weight})
|
||||||
|
seen[data.Original] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort matches by weight (descending)
|
||||||
|
sort.Slice(matches, func(i, j int) bool {
|
||||||
|
return matches[i].weight > matches[j].weight
|
||||||
|
})
|
||||||
|
|
||||||
|
// Extract just the soundtrack names in order
|
||||||
|
result := make([]string, len(matches))
|
||||||
|
for i, m := range matches {
|
||||||
|
result[i] = m.item
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindSoundtrackFuzzyWebHandler handles fuzzy search requests
|
||||||
|
func FindSoundtrackFuzzyWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
search_term := r.FormValue("search_term")
|
||||||
|
|
||||||
|
results := fuzzySearch(search_term)
|
||||||
|
|
||||||
|
component := FoundSoundtracks(results)
|
||||||
|
err = component.Render(r.Context(), w)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
log.Fatalf("Error rendering in FindSoundtrackFuzzyWebHandler: %e", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,45 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
templ HelloForm() {
|
templ SearchForm() {
|
||||||
@Base() {
|
@Base() {
|
||||||
<button id="dark-mode-toggle">🌙</button>
|
<button id="dark-mode-toggle">🌙</button>
|
||||||
<div id="search-container">
|
<div id="search-container">
|
||||||
<input id="search_term" name="search_term" type="text" hx-post="/find" hx-trigger="keyup changed delay:0.25s" hx-target="#soundtracks-container"/>
|
<input id="search_term" name="search_term" type="text" hx-post="/findfuzzy" hx-trigger="keyup changed delay:0.25s" hx-target="#soundtracks-container"/>
|
||||||
|
<div class="radio-group" style="display: inline-block; margin-left: 10px;">
|
||||||
|
<label><input type="radio" name="search_type" value="fuzzy" checked> Fuzzy</label>
|
||||||
|
<label><input type="radio" name="search_type" value="normal"> Normal</label>
|
||||||
|
</div>
|
||||||
<button type="button" id="clear" name="clear">Clear</button>
|
<button type="button" id="clear" name="clear">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="soundtracks-container"></div>
|
<div id="soundtracks-container"></div>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('readystatechange', () => {
|
// Get current search type from radio buttons
|
||||||
|
function getSearchType() {
|
||||||
|
return document.querySelector('input[name="search_type"]:checked').value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get endpoint based on search type
|
||||||
|
function getSearchEndpoint() {
|
||||||
|
return getSearchType() === 'fuzzy' ? '/findfuzzy' : '/find';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update search input endpoint
|
||||||
|
function updateSearchEndpoint() {
|
||||||
|
const endpoint = getSearchEndpoint();
|
||||||
|
document.getElementById('search_term').setAttribute('hx-post', endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('readystatechange', () => {
|
||||||
if (document.readyState == 'complete') {
|
if (document.readyState == 'complete') {
|
||||||
htmx.ajax('POST', '/find', '#soundtracks-container');
|
// Initialize with fuzzy search (default)
|
||||||
|
htmlx.ajax('POST', '/findfuzzy', '#soundtracks-container');
|
||||||
document.getElementById("search_term").focus();
|
document.getElementById("search_term").focus();
|
||||||
|
|
||||||
|
// Add event listeners for radio buttons
|
||||||
|
document.querySelectorAll('input[name="search_type"]').forEach(radio => {
|
||||||
|
radio.addEventListener('change', updateSearchEndpoint);
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize dark mode from localStorage (default to dark)
|
// Initialize dark mode from localStorage (default to dark)
|
||||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
if (savedTheme === 'dark') {
|
if (savedTheme === 'dark') {
|
||||||
@@ -38,7 +64,8 @@ templ HelloForm() {
|
|||||||
|
|
||||||
document.getElementById("clear").addEventListener("click", function (event) {
|
document.getElementById("clear").addEventListener("click", function (event) {
|
||||||
document.getElementById("search_term").value = "";
|
document.getElementById("search_term").value = "";
|
||||||
htmx.ajax('POST', '/find', '#soundtracks-container');
|
// Use current search type endpoint
|
||||||
|
htmlx.ajax('POST', getSearchEndpoint(), '#soundtracks-container');
|
||||||
document.getElementById("search_term").focus();
|
document.getElementById("search_term").focus();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -51,4 +78,4 @@ templ FoundSoundtracks(soundtracks []string) {
|
|||||||
<p class="soundtrack-text">{ soundtrack }</p>
|
<p class="soundtrack-text">{ soundtrack }</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+22
-54
@@ -175,17 +175,17 @@ func SyncResult() SyncResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncSoundtracksNewFull() {
|
func SyncSoundtracksFull() {
|
||||||
syncSoundtracksNew(true)
|
syncSoundtracks(true)
|
||||||
Reset()
|
Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncSoundtracksNewOnlyChanges() {
|
func SyncSoundtracksOnlyChanges() {
|
||||||
syncSoundtracksNew(false)
|
syncSoundtracks(false)
|
||||||
Reset()
|
Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncSoundtracksNew(full bool) {
|
func syncSoundtracks(full bool) {
|
||||||
musicPath := os.Getenv("MUSIC_PATH")
|
musicPath := os.Getenv("MUSIC_PATH")
|
||||||
fmt.Printf("dir: %s\n", musicPath)
|
fmt.Printf("dir: %s\n", musicPath)
|
||||||
logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath))
|
logging.GetLogger().Debug("Folder to sync", zap.String("MUSIC_PATH", musicPath))
|
||||||
@@ -233,11 +233,11 @@ func syncSoundtracksNew(full bool) {
|
|||||||
for _, dir := range directories {
|
for _, dir := range directories {
|
||||||
pool.Submit(func() {
|
pool.Submit(func() {
|
||||||
defer syncWg.Done()
|
defer syncWg.Done()
|
||||||
syncSoundtrackNew(dir, foldersToSkip, musicPath, full)
|
syncSoundtrack(dir, foldersToSkip, musicPath, full)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
syncWg.Wait()
|
syncWg.Wait()
|
||||||
checkBrokenSongsNew()
|
checkBrokenSongs()
|
||||||
|
|
||||||
soundtracksAfterSync, err = repo.FindAllSoundtracks(BackendCtx())
|
soundtracksAfterSync, err = repo.FindAllSoundtracks(BackendCtx())
|
||||||
handleError("FindAllSoundtracks After", err, "")
|
handleError("FindAllSoundtracks After", err, "")
|
||||||
@@ -250,7 +250,7 @@ func syncSoundtracksNew(full bool) {
|
|||||||
Syncing = false
|
Syncing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBrokenSongsNew() {
|
func checkBrokenSongs() {
|
||||||
allSongs, err := repo.FetchAllSongs(BackendCtx())
|
allSongs, err := repo.FetchAllSongs(BackendCtx())
|
||||||
handleError("FetchAllSongs", err, "")
|
handleError("FetchAllSongs", err, "")
|
||||||
var brokenWg sync.WaitGroup
|
var brokenWg sync.WaitGroup
|
||||||
@@ -261,7 +261,7 @@ func checkBrokenSongsNew() {
|
|||||||
for _, song := range allSongs {
|
for _, song := range allSongs {
|
||||||
poolBroken.Submit(func() {
|
poolBroken.Submit(func() {
|
||||||
defer brokenWg.Done()
|
defer brokenWg.Done()
|
||||||
checkBrokenSongNew(song)
|
checkBrokenSong(song)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
brokenWg.Wait()
|
brokenWg.Wait()
|
||||||
@@ -271,7 +271,7 @@ func checkBrokenSongsNew() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkBrokenSongNew(song repository.Song) {
|
func checkBrokenSong(song repository.Song) {
|
||||||
//Check if file exists and open
|
//Check if file exists and open
|
||||||
openFile, err := os.Open(song.Path)
|
openFile, err := os.Open(song.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -286,7 +286,7 @@ func checkBrokenSongNew(song repository.Song) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncSoundtrackNew(file os.DirEntry, foldersToSkip []string, baseDir string, full bool) {
|
func syncSoundtrack(file os.DirEntry, foldersToSkip []string, baseDir string, full bool) {
|
||||||
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
|
if file.IsDir() && !contains(foldersToSkip, file.Name()) {
|
||||||
logging.GetLogger().Debug("Syncing soundtrack", zap.String("soundtrack", file.Name()))
|
logging.GetLogger().Debug("Syncing soundtrack", zap.String("soundtrack", file.Name()))
|
||||||
soundtrackDir := baseDir + file.Name() + "/"
|
soundtrackDir := baseDir + file.Name() + "/"
|
||||||
@@ -328,46 +328,14 @@ func syncSoundtrackNew(file os.DirEntry, foldersToSkip []string, baseDir string,
|
|||||||
}
|
}
|
||||||
switch status {
|
switch status {
|
||||||
case NewSoundtrack:
|
case NewSoundtrack:
|
||||||
if id != -1 {
|
id = insertSoundtrack(file.Name(), soundtrackDir, dirHash)
|
||||||
for _, entry := range entries {
|
|
||||||
fileInfo, err := entry.Info()
|
|
||||||
if err != nil {
|
|
||||||
logging.GetLogger().Error("Failed to get file info", zap.String("error", err.Error()))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
id = getIdFromFileNew(fileInfo)
|
|
||||||
if id != -1 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = repo.InsertSoundtrackWithExistingId(BackendCtx(), repository.InsertSoundtrackWithExistingIdParams{ID: id, SoundtrackName: file.Name(), Path: soundtrackDir, Hash: dirHash})
|
|
||||||
handleError("InsertSoundtrackWithExistingId", err, "")
|
|
||||||
if err != nil {
|
|
||||||
logging.GetLogger().Debug("Soundtrack already exists, removing old ID file",
|
|
||||||
zap.Int32("id", id),
|
|
||||||
zap.String("soundtrack_dir", soundtrackDir))
|
|
||||||
fileName := soundtrackDir + "/." + strconv.Itoa(int(id)) + ".id"
|
|
||||||
logging.GetLogger().Debug("Removing ID file", zap.String("filename", fileName))
|
|
||||||
|
|
||||||
err := os.Remove(fileName)
|
|
||||||
if err != nil {
|
|
||||||
logging.GetLogger().Error("Failed to remove ID file", zap.String("filename", fileName), zap.String("error", err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
newDirHash := getHashForDir(soundtrackDir)
|
|
||||||
|
|
||||||
id = insertSoundtrackNew(file.Name(), soundtrackDir, newDirHash)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
id = insertSoundtrackNew(file.Name(), soundtrackDir, dirHash)
|
|
||||||
}
|
|
||||||
logging.GetLogger().Debug("New soundtrack detected",
|
logging.GetLogger().Debug("New soundtrack detected",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
zap.String("soundtrack", file.Name()),
|
zap.String("soundtrack", file.Name()),
|
||||||
zap.String("hash", dirHash),
|
zap.String("hash", dirHash),
|
||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
soundtracksAdded = append(soundtracksAdded, file.Name())
|
soundtracksAdded = append(soundtracksAdded, file.Name())
|
||||||
newCheckSongs(entries, soundtrackDir, id)
|
checkSongs(entries, soundtrackDir, id)
|
||||||
case SoundtrackChanged:
|
case SoundtrackChanged:
|
||||||
logging.GetLogger().Debug("Soundtrack changed",
|
logging.GetLogger().Debug("Soundtrack changed",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
@@ -377,7 +345,7 @@ func syncSoundtrackNew(file os.DirEntry, foldersToSkip []string, baseDir string,
|
|||||||
err = repo.UpdateSoundtrackHash(BackendCtx(), repository.UpdateSoundtrackHashParams{Hash: dirHash, ID: id})
|
err = repo.UpdateSoundtrackHash(BackendCtx(), repository.UpdateSoundtrackHashParams{Hash: dirHash, ID: id})
|
||||||
handleError("UpdateSoundtrackHash", err, "")
|
handleError("UpdateSoundtrackHash", err, "")
|
||||||
soundtracksChangedContent = append(soundtracksChangedContent, file.Name())
|
soundtracksChangedContent = append(soundtracksChangedContent, file.Name())
|
||||||
newCheckSongs(entries, soundtrackDir, id)
|
checkSongs(entries, soundtrackDir, id)
|
||||||
case TitleChanged:
|
case TitleChanged:
|
||||||
logging.GetLogger().Debug("Soundtrack title changed",
|
logging.GetLogger().Debug("Soundtrack title changed",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
@@ -387,7 +355,7 @@ func syncSoundtrackNew(file os.DirEntry, foldersToSkip []string, baseDir string,
|
|||||||
zap.String("status", status.String()))
|
zap.String("status", status.String()))
|
||||||
err = repo.UpdateSoundtrackName(BackendCtx(), repository.UpdateSoundtrackNameParams{Name: file.Name(), Path: soundtrackDir, ID: id})
|
err = repo.UpdateSoundtrackName(BackendCtx(), repository.UpdateSoundtrackNameParams{Name: file.Name(), Path: soundtrackDir, ID: id})
|
||||||
handleError("UpdateSoundtrackName", err, "")
|
handleError("UpdateSoundtrackName", err, "")
|
||||||
newCheckSongs(entries, soundtrackDir, id)
|
checkSongs(entries, soundtrackDir, id)
|
||||||
if soundtracksChangedTitle == nil {
|
if soundtracksChangedTitle == nil {
|
||||||
soundtracksChangedTitle = make(map[string]string)
|
soundtracksChangedTitle = make(map[string]string)
|
||||||
}
|
}
|
||||||
@@ -405,7 +373,7 @@ func syncSoundtrackNew(file os.DirEntry, foldersToSkip []string, baseDir string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
newCheckSongs(entries, soundtrackDir, id)
|
checkSongs(entries, soundtrackDir, id)
|
||||||
soundtracksReAdded = append(soundtracksReAdded, file.Name())
|
soundtracksReAdded = append(soundtracksReAdded, file.Name())
|
||||||
logging.GetLogger().Debug("Soundtrack added again",
|
logging.GetLogger().Debug("Soundtrack added again",
|
||||||
zap.Int32("id", id),
|
zap.Int32("id", id),
|
||||||
@@ -430,7 +398,7 @@ func syncSoundtrackNew(file os.DirEntry, foldersToSkip []string, baseDir string,
|
|||||||
zap.Int("percent", int((foldersSynced/numberOfFoldersToSync)*100)))
|
zap.Int("percent", int((foldersSynced/numberOfFoldersToSync)*100)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertSoundtrackNew(name string, path string, hash string) int32 {
|
func insertSoundtrack(name string, path string, hash string) int32 {
|
||||||
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
|
var duplicateError = errors.New("ERROR: duplicate key value violates unique")
|
||||||
id, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{SoundtrackName: name, Path: path, Hash: hash})
|
id, err := repo.InsertSoundtrack(BackendCtx(), repository.InsertSoundtrackParams{SoundtrackName: name, Path: path, Hash: hash})
|
||||||
handleError("InsertSoundtrack", err, "")
|
handleError("InsertSoundtrack", err, "")
|
||||||
@@ -440,14 +408,14 @@ func insertSoundtrackNew(name string, path string, hash string) int32 {
|
|||||||
logging.GetLogger().Debug("Resetting soundtrack ID sequence")
|
logging.GetLogger().Debug("Resetting soundtrack ID sequence")
|
||||||
_, err = repo.ResetSoundtrackIdSeq(BackendCtx())
|
_, err = repo.ResetSoundtrackIdSeq(BackendCtx())
|
||||||
handleError("ResetSoundtrackIdSeq", err, "")
|
handleError("ResetSoundtrackIdSeq", err, "")
|
||||||
id = insertSoundtrackNew(name, path, hash)
|
id = insertSoundtrack(name, path, hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return id
|
return id
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCheckSongs(entries []os.DirEntry, soundtrackDir string, id int32) int32 {
|
func checkSongs(entries []os.DirEntry, soundtrackDir string, id int32) int32 {
|
||||||
//hasher := md5.New()
|
//hasher := md5.New()
|
||||||
var numberOfSongs int32
|
var numberOfSongs int32
|
||||||
numberOfFiles := len(entries)
|
numberOfFiles := len(entries)
|
||||||
@@ -457,7 +425,7 @@ func newCheckSongs(entries []os.DirEntry, soundtrackDir string, id int32) int32
|
|||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
poolSong.Submit(func() {
|
poolSong.Submit(func() {
|
||||||
defer songWg.Done()
|
defer songWg.Done()
|
||||||
if newCheckSong(entry, soundtrackDir, id) {
|
if checkSong(entry, soundtrackDir, id) {
|
||||||
numberOfSongs++
|
numberOfSongs++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -466,7 +434,7 @@ func newCheckSongs(entries []os.DirEntry, soundtrackDir string, id int32) int32
|
|||||||
return numberOfSongs
|
return numberOfSongs
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCheckSong(entry os.DirEntry, soundtrackDir string, id int32) bool {
|
func checkSong(entry os.DirEntry, soundtrackDir string, id int32) bool {
|
||||||
fileInfo, err := entry.Info()
|
fileInfo, err := entry.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.GetLogger().Error("Failed to get file info", zap.String("filename", entry.Name()), zap.String("error", err.Error()))
|
logging.GetLogger().Error("Failed to get file info", zap.String("filename", entry.Name()), zap.String("error", err.Error()))
|
||||||
@@ -570,7 +538,7 @@ func getHashForFile(path string) string {
|
|||||||
return hex.EncodeToString(hasher.Sum(nil))
|
return hex.EncodeToString(hasher.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIdFromFileNew(file os.FileInfo) int32 {
|
func getIdFromFile(file os.FileInfo) int32 {
|
||||||
name := file.Name()
|
name := file.Name()
|
||||||
if !file.IsDir() && strings.HasSuffix(name, ".id") {
|
if !file.IsDir() && strings.HasSuffix(name, ".id") {
|
||||||
name = strings.Replace(name, ".id", "", 1)
|
name = strings.Replace(name, ".id", "", 1)
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
|
|
||||||
func TestContains(t *testing.T) {
|
func TestContains(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
slice []string
|
slice []string
|
||||||
search string
|
search string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "element exists",
|
name: "element exists",
|
||||||
@@ -155,9 +155,9 @@ func TestGetIdFromFileNew(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := getIdFromFileNew(tt.fileInfo)
|
result := getIdFromFile(tt.fileInfo)
|
||||||
if result != tt.expected {
|
if result != tt.expected {
|
||||||
t.Errorf("getIdFromFileNew() = %v, want %v", result, tt.expected)
|
t.Errorf("getIdFromFile() = %v, want %v", result, tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -172,10 +172,10 @@ type mockFileInfoForSong struct {
|
|||||||
|
|
||||||
func (m *mockFileInfoForSong) Name() string { return m.name }
|
func (m *mockFileInfoForSong) Name() string { return m.name }
|
||||||
func (m *mockFileInfoForSong) Size() int64 { return m.size }
|
func (m *mockFileInfoForSong) Size() int64 { return m.size }
|
||||||
func (m *mockFileInfoForSong) Mode() os.FileMode { return 0 }
|
func (m *mockFileInfoForSong) Mode() os.FileMode { return 0 }
|
||||||
func (m *mockFileInfoForSong) ModTime() time.Time { return time.Time{} }
|
func (m *mockFileInfoForSong) ModTime() time.Time { return time.Time{} }
|
||||||
func (m *mockFileInfoForSong) IsDir() bool { return m.isDir }
|
func (m *mockFileInfoForSong) IsDir() bool { return m.isDir }
|
||||||
func (m *mockFileInfoForSong) Sys() interface{} { return nil }
|
func (m *mockFileInfoForSong) Sys() interface{} { return nil }
|
||||||
|
|
||||||
type mockFileInfoForCover struct {
|
type mockFileInfoForCover struct {
|
||||||
name string
|
name string
|
||||||
@@ -185,10 +185,10 @@ type mockFileInfoForCover struct {
|
|||||||
|
|
||||||
func (m *mockFileInfoForCover) Name() string { return m.name }
|
func (m *mockFileInfoForCover) Name() string { return m.name }
|
||||||
func (m *mockFileInfoForCover) Size() int64 { return m.size }
|
func (m *mockFileInfoForCover) Size() int64 { return m.size }
|
||||||
func (m *mockFileInfoForCover) Mode() os.FileMode { return 0 }
|
func (m *mockFileInfoForCover) Mode() os.FileMode { return 0 }
|
||||||
func (m *mockFileInfoForCover) ModTime() time.Time { return time.Time{} }
|
func (m *mockFileInfoForCover) ModTime() time.Time { return time.Time{} }
|
||||||
func (m *mockFileInfoForCover) IsDir() bool { return m.isDir }
|
func (m *mockFileInfoForCover) IsDir() bool { return m.isDir }
|
||||||
func (m *mockFileInfoForCover) Sys() interface{} { return nil }
|
func (m *mockFileInfoForCover) Sys() interface{} { return nil }
|
||||||
|
|
||||||
type mockFileInfoForId struct {
|
type mockFileInfoForId struct {
|
||||||
name string
|
name string
|
||||||
@@ -198,7 +198,7 @@ type mockFileInfoForId struct {
|
|||||||
|
|
||||||
func (m *mockFileInfoForId) Name() string { return m.name }
|
func (m *mockFileInfoForId) Name() string { return m.name }
|
||||||
func (m *mockFileInfoForId) Size() int64 { return m.size }
|
func (m *mockFileInfoForId) Size() int64 { return m.size }
|
||||||
func (m *mockFileInfoForId) Mode() os.FileMode { return 0 }
|
func (m *mockFileInfoForId) Mode() os.FileMode { return 0 }
|
||||||
func (m *mockFileInfoForId) ModTime() time.Time { return time.Time{} }
|
func (m *mockFileInfoForId) ModTime() time.Time { return time.Time{} }
|
||||||
func (m *mockFileInfoForId) IsDir() bool { return m.isDir }
|
func (m *mockFileInfoForId) IsDir() bool { return m.isDir }
|
||||||
func (m *mockFileInfoForId) Sys() interface{} { return nil }
|
func (m *mockFileInfoForId) Sys() interface{} { return nil }
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"music-server/internal/backend"
|
||||||
|
"music-server/internal/logging"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SyncCheckMiddleware blocks requests when syncing is in progress.
|
||||||
|
// It returns HTTP 423 Locked with a standard message.
|
||||||
|
// This middleware should be applied to handlers that cannot execute during sync.
|
||||||
|
func SyncCheckMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c *echo.Context) error {
|
||||||
|
if backend.Syncing {
|
||||||
|
logging.GetLogger().Info("Syncing is in progress - request blocked")
|
||||||
|
return c.JSON(http.StatusLocked, "Syncing is in progress")
|
||||||
|
}
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,10 +31,6 @@ func NewMusicHandler() *MusicHandler {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music [get]
|
// @Router /music [get]
|
||||||
func (m *MusicHandler) GetSong(ctx *echo.Context) error {
|
func (m *MusicHandler) GetSong(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
song := ctx.QueryParam("song")
|
song := ctx.QueryParam("song")
|
||||||
if song == "" {
|
if song == "" {
|
||||||
return ctx.String(http.StatusBadRequest, "song can't be empty")
|
return ctx.String(http.StatusBadRequest, "song can't be empty")
|
||||||
@@ -58,10 +54,6 @@ func (m *MusicHandler) GetSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/soundTest [get]
|
// @Router /music/soundTest [get]
|
||||||
func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
|
func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
songPath := backend.GetSoundCheckSong()
|
songPath := backend.GetSoundCheckSong()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -80,10 +72,6 @@ func (m *MusicHandler) GetSoundCheckSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/reset [get]
|
// @Router /music/reset [get]
|
||||||
func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
|
func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
backend.Reset()
|
backend.Reset()
|
||||||
return ctx.NoContent(http.StatusOK)
|
return ctx.NoContent(http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -98,10 +86,6 @@ func (m *MusicHandler) ResetMusic(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/rand [get]
|
// @Router /music/rand [get]
|
||||||
func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
|
func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
songPath := backend.GetRandomSong()
|
songPath := backend.GetRandomSong()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,10 +105,6 @@ func (m *MusicHandler) GetRandomSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/rand/low [get]
|
// @Router /music/rand/low [get]
|
||||||
func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
|
func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
songPath := backend.GetRandomSongLowChance()
|
songPath := backend.GetRandomSongLowChance()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -144,10 +124,6 @@ func (m *MusicHandler) GetRandomSongLowChance(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/rand/classic [get]
|
// @Router /music/rand/classic [get]
|
||||||
func (m *MusicHandler) GetRandomSongClassic(ctx *echo.Context) error {
|
func (m *MusicHandler) GetRandomSongClassic(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
songPath := backend.GetRandomSongClassic()
|
songPath := backend.GetRandomSongClassic()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,10 +169,6 @@ func (m *MusicHandler) GetPlayedSongs(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/next [get]
|
// @Router /music/next [get]
|
||||||
func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
|
func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
songPath := backend.GetNextSong()
|
songPath := backend.GetNextSong()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -216,10 +188,6 @@ func (m *MusicHandler) GetNextSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/previous [get]
|
// @Router /music/previous [get]
|
||||||
func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
songPath := backend.GetPreviousSong()
|
songPath := backend.GetPreviousSong()
|
||||||
file, err := os.Open(songPath)
|
file, err := os.Open(songPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -239,10 +207,6 @@ func (m *MusicHandler) GetPreviousSong(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/all/order [get]
|
// @Router /music/all/order [get]
|
||||||
func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
|
func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
soundtrackList := backend.GetAllSoundtracks()
|
soundtrackList := backend.GetAllSoundtracks()
|
||||||
return ctx.JSON(http.StatusOK, soundtrackList)
|
return ctx.JSON(http.StatusOK, soundtrackList)
|
||||||
}
|
}
|
||||||
@@ -257,10 +221,6 @@ func (m *MusicHandler) GetAllSoundtracks(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/all/random [get]
|
// @Router /music/all/random [get]
|
||||||
func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
soundtrackList := backend.GetAllSoundtracksRandom()
|
soundtrackList := backend.GetAllSoundtracksRandom()
|
||||||
return ctx.JSON(http.StatusOK, soundtrackList)
|
return ctx.JSON(http.StatusOK, soundtrackList)
|
||||||
}
|
}
|
||||||
@@ -277,10 +237,6 @@ func (m *MusicHandler) GetAllSoundtracksRandom(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/played [put]
|
// @Router /music/played [put]
|
||||||
func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
|
func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
song, err := strconv.Atoi(ctx.QueryParam("song"))
|
song, err := strconv.Atoi(ctx.QueryParam("song"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ctx.JSON(http.StatusBadRequest, err.Error())
|
return ctx.JSON(http.StatusBadRequest, err.Error())
|
||||||
@@ -299,10 +255,6 @@ func (m *MusicHandler) PutPlayed(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/addQue [get]
|
// @Router /music/addQue [get]
|
||||||
func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
|
func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
backend.AddLatestToQue()
|
backend.AddLatestToQue()
|
||||||
return ctx.NoContent(http.StatusOK)
|
return ctx.NoContent(http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -316,10 +268,6 @@ func (m *MusicHandler) AddLatestToQue(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /music/addPlayed [get]
|
// @Router /music/addPlayed [get]
|
||||||
func (m *MusicHandler) AddLatestPlayed(ctx *echo.Context) error {
|
func (m *MusicHandler) AddLatestPlayed(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Info("Syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
backend.AddLatestPlayed()
|
backend.AddLatestPlayed()
|
||||||
return ctx.NoContent(http.StatusOK)
|
return ctx.NoContent(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-21
@@ -50,8 +50,9 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
fileServer := http.FileServer(http.FS(web.Assets))
|
fileServer := http.FileServer(http.FS(web.Assets))
|
||||||
e.GET("/assets/*", echo.WrapHandler(fileServer))
|
e.GET("/assets/*", echo.WrapHandler(fileServer))
|
||||||
|
|
||||||
e.GET("/search", echo.WrapHandler(templ.Handler(web.HelloForm())))
|
e.GET("/search", echo.WrapHandler(templ.Handler(web.SearchForm())))
|
||||||
e.POST("/find", echo.WrapHandler(http.HandlerFunc(web.FindSoundtrackWebHandler)))
|
e.POST("/find", echo.WrapHandler(http.HandlerFunc(web.FindSoundtrackWebHandler)))
|
||||||
|
e.POST("/findfuzzy", echo.WrapHandler(http.HandlerFunc(web.FindSoundtrackFuzzyWebHandler)))
|
||||||
|
|
||||||
e.Static("/", "/frontend")
|
e.Static("/", "/frontend")
|
||||||
|
|
||||||
@@ -82,32 +83,32 @@ func (s *Server) RegisterRoutes() http.Handler {
|
|||||||
|
|
||||||
sync := NewSyncHandler()
|
sync := NewSyncHandler()
|
||||||
syncGroup := e.Group("/sync")
|
syncGroup := e.Group("/sync")
|
||||||
syncGroup.GET("", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
syncGroup.GET("", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
||||||
syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress))
|
syncGroup.GET("/progress", deprecatedMiddleware(sync.SyncProgress))
|
||||||
syncGroup.GET("/new", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
syncGroup.GET("/new", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
||||||
syncGroup.GET("/full", deprecatedMiddleware(sync.SyncSoundtracksNewFull))
|
syncGroup.GET("/full", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewFull)))
|
||||||
syncGroup.GET("/new/full", deprecatedMiddleware(sync.SyncSoundtracksNewFull))
|
syncGroup.GET("/new/full", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewFull)))
|
||||||
syncGroup.GET("/quick", deprecatedMiddleware(sync.SyncSoundtracksNewOnlyChanges))
|
syncGroup.GET("/quick", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.SyncSoundtracksNewOnlyChanges)))
|
||||||
syncGroup.GET("/reset", deprecatedMiddleware(sync.ResetDB))
|
syncGroup.GET("/reset", deprecatedMiddleware(middleware.SyncCheckMiddleware(sync.ResetDB)))
|
||||||
|
|
||||||
music := NewMusicHandler()
|
music := NewMusicHandler()
|
||||||
musicGroup := e.Group("/music")
|
musicGroup := e.Group("/music")
|
||||||
musicGroup.GET("", deprecatedMiddleware(music.GetSong))
|
musicGroup.GET("", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetSong)))
|
||||||
musicGroup.GET("/soundTest", deprecatedMiddleware(music.GetSoundCheckSong))
|
musicGroup.GET("/soundTest", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetSoundCheckSong)))
|
||||||
musicGroup.GET("/reset", deprecatedMiddleware(music.ResetMusic))
|
musicGroup.GET("/reset", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.ResetMusic)))
|
||||||
musicGroup.GET("/rand", deprecatedMiddleware(music.GetRandomSong))
|
musicGroup.GET("/rand", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSong)))
|
||||||
musicGroup.GET("/rand/low", deprecatedMiddleware(music.GetRandomSongLowChance))
|
musicGroup.GET("/rand/low", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSongLowChance)))
|
||||||
musicGroup.GET("/rand/classic", deprecatedMiddleware(music.GetRandomSongClassic))
|
musicGroup.GET("/rand/classic", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetRandomSongClassic)))
|
||||||
musicGroup.GET("/info", deprecatedMiddleware(music.GetSongInfo))
|
musicGroup.GET("/info", deprecatedMiddleware(music.GetSongInfo))
|
||||||
musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs))
|
musicGroup.GET("/list", deprecatedMiddleware(music.GetPlayedSongs))
|
||||||
musicGroup.GET("/next", deprecatedMiddleware(music.GetNextSong))
|
musicGroup.GET("/next", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetNextSong)))
|
||||||
musicGroup.GET("/previous", deprecatedMiddleware(music.GetPreviousSong))
|
musicGroup.GET("/previous", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetPreviousSong)))
|
||||||
musicGroup.GET("/all", deprecatedMiddleware(music.GetAllSoundtracksRandom))
|
musicGroup.GET("/all", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracksRandom)))
|
||||||
musicGroup.GET("/all/order", deprecatedMiddleware(music.GetAllSoundtracks))
|
musicGroup.GET("/all/order", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracks)))
|
||||||
musicGroup.GET("/all/random", deprecatedMiddleware(music.GetAllSoundtracksRandom))
|
musicGroup.GET("/all/random", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.GetAllSoundtracksRandom)))
|
||||||
musicGroup.PUT("/played", deprecatedMiddleware(music.PutPlayed))
|
musicGroup.PUT("/played", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.PutPlayed)))
|
||||||
musicGroup.GET("/addQue", deprecatedMiddleware(music.AddLatestToQue))
|
musicGroup.GET("/addQue", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.AddLatestToQue)))
|
||||||
musicGroup.GET("/addPlayed", deprecatedMiddleware(music.AddLatestPlayed))
|
musicGroup.GET("/addPlayed", deprecatedMiddleware(middleware.SyncCheckMiddleware(music.AddLatestPlayed)))
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// API v1 Routes with Token Authentication
|
// API v1 Routes with Token Authentication
|
||||||
|
|||||||
@@ -44,13 +44,9 @@ func (s *SyncHandler) SyncProgress(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync [get]
|
// @Router /sync [get]
|
||||||
func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error {
|
func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Warn("Syncing is already in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
logging.GetLogger().Info("Starting sync with only changes")
|
logging.GetLogger().Info("Starting sync with only changes")
|
||||||
backend.Syncing = true
|
backend.Syncing = true
|
||||||
go backend.SyncSoundtracksNewOnlyChanges()
|
go backend.SyncSoundtracksOnlyChanges()
|
||||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks")
|
return ctx.JSON(http.StatusOK, "Start syncing soundtracks")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,13 +60,9 @@ func (s *SyncHandler) SyncSoundtracksNewOnlyChanges(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync/full [get]
|
// @Router /sync/full [get]
|
||||||
func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error {
|
func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Warn("Syncing is already in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
logging.GetLogger().Info("Starting full sync")
|
logging.GetLogger().Info("Starting full sync")
|
||||||
backend.Syncing = true
|
backend.Syncing = true
|
||||||
go backend.SyncSoundtracksNewFull()
|
go backend.SyncSoundtracksFull()
|
||||||
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
|
return ctx.JSON(http.StatusOK, "Start syncing soundtracks full")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,10 +76,6 @@ func (s *SyncHandler) SyncSoundtracksNewFull(ctx *echo.Context) error {
|
|||||||
// @Failure 423 {string} string "Syncing is in progress"
|
// @Failure 423 {string} string "Syncing is in progress"
|
||||||
// @Router /sync/reset [get]
|
// @Router /sync/reset [get]
|
||||||
func (s *SyncHandler) ResetDB(ctx *echo.Context) error {
|
func (s *SyncHandler) ResetDB(ctx *echo.Context) error {
|
||||||
if backend.Syncing {
|
|
||||||
logging.GetLogger().Warn("Cannot reset - syncing is in progress")
|
|
||||||
return ctx.JSON(http.StatusLocked, "Syncing is in progress")
|
|
||||||
}
|
|
||||||
logging.GetLogger().Info("Resetting soundtracks database")
|
logging.GetLogger().Info("Resetting soundtracks database")
|
||||||
backend.ResetDB()
|
backend.ResetDB()
|
||||||
return ctx.JSON(http.StatusOK, "Soundtracks and songs are deleted from the database")
|
return ctx.JSON(http.StatusOK, "Soundtracks and songs are deleted from the database")
|
||||||
|
|||||||
Reference in New Issue
Block a user