This commit is contained in:
parent
1acd1c0cab
commit
7cd5e80468
23 changed files with 988 additions and 5 deletions
26
cache.go
26
cache.go
|
@ -62,6 +62,18 @@ type ForumSearchResult struct {
|
|||
ThumbnailSrc string `json:"thumbnailSrc,omitempty"`
|
||||
}
|
||||
|
||||
type MusicResult struct {
|
||||
URL string
|
||||
Title string
|
||||
Artist string
|
||||
Description string
|
||||
PublishedDate string
|
||||
Thumbnail string
|
||||
// AudioURL string
|
||||
Source string
|
||||
Duration string
|
||||
}
|
||||
|
||||
// GeocodeCachedItem represents a geocoding result stored in the cache.
|
||||
type GeocodeCachedItem struct {
|
||||
Latitude string
|
||||
|
@ -284,15 +296,23 @@ func convertToSearchResults(results interface{}) []SearchResult {
|
|||
genericResults[i] = r
|
||||
}
|
||||
return genericResults
|
||||
case []MusicResult:
|
||||
genericResults := make([]SearchResult, len(res))
|
||||
for i, r := range res {
|
||||
genericResults[i] = r
|
||||
}
|
||||
return genericResults
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertToSpecificResults(results []SearchResult) ([]TextSearchResult, []TorrentResult, []ImageSearchResult, []ForumSearchResult) {
|
||||
func convertToSpecificResults(results []SearchResult) ([]TextSearchResult, []TorrentResult, []ImageSearchResult, []ForumSearchResult, []MusicResult) {
|
||||
var textResults []TextSearchResult
|
||||
var torrentResults []TorrentResult
|
||||
var imageResults []ImageSearchResult
|
||||
var forumResults []ForumSearchResult
|
||||
var musicResults []MusicResult
|
||||
|
||||
for _, r := range results {
|
||||
switch res := r.(type) {
|
||||
case TextSearchResult:
|
||||
|
@ -303,7 +323,9 @@ func convertToSpecificResults(results []SearchResult) ([]TextSearchResult, []Tor
|
|||
imageResults = append(imageResults, res)
|
||||
case ForumSearchResult:
|
||||
forumResults = append(forumResults, res)
|
||||
case MusicResult:
|
||||
musicResults = append(musicResults, res)
|
||||
}
|
||||
}
|
||||
return textResults, torrentResults, imageResults, forumResults
|
||||
return textResults, torrentResults, imageResults, forumResults, musicResults
|
||||
}
|
||||
|
|
2
files.go
2
files.go
|
@ -111,7 +111,7 @@ func getFileResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string,
|
|||
printDebug("Crawler disabled; skipping fetching.")
|
||||
}
|
||||
} else {
|
||||
_, torrentResults, _, _ := convertToSpecificResults(results)
|
||||
_, torrentResults, _, _, _ := convertToSpecificResults(results)
|
||||
combinedResults = torrentResults
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
|
|
|
@ -107,7 +107,7 @@ func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string
|
|||
printDebug("Crawler disabled; skipping fetching from image search engines.")
|
||||
}
|
||||
} else {
|
||||
_, _, imageResults, _ := convertToSpecificResults(results)
|
||||
_, _, imageResults, _, _ := convertToSpecificResults(results)
|
||||
combinedResults = filterValidImages(imageResults)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
|
|
1
init.go
1
init.go
|
@ -81,6 +81,7 @@ func main() {
|
|||
initImageEngines()
|
||||
initFileEngines()
|
||||
initPipedInstances()
|
||||
initMusicEngines()
|
||||
}
|
||||
|
||||
InitializeLanguage("en") // Initialize language before generating OpenSearch
|
||||
|
|
|
@ -88,6 +88,9 @@ msgstr "Video"
|
|||
msgid "videos"
|
||||
msgstr "Videos"
|
||||
|
||||
msgid "music"
|
||||
msgstr "Music"
|
||||
|
||||
msgid "forum"
|
||||
msgstr "Forum"
|
||||
|
||||
|
|
2
main.go
2
main.go
|
@ -164,6 +164,8 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
|
|||
handleImageSearch(w, r, settings, query, page)
|
||||
case "video":
|
||||
handleVideoSearch(w, settings, query, page)
|
||||
case "music":
|
||||
handleMusicSearch(w, settings, query, page)
|
||||
case "map":
|
||||
handleMapSearch(w, settings, query)
|
||||
case "forum":
|
||||
|
|
72
music-bandcamp.go
Normal file
72
music-bandcamp.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
// music-bandcamp.go - Bandcamp specific implementation
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func SearchBandcamp(query string, page int) ([]MusicResult, error) {
|
||||
baseURL := "https://bandcamp.com/search?"
|
||||
params := url.Values{
|
||||
"q": []string{query},
|
||||
"page": []string{fmt.Sprintf("%d", page)},
|
||||
}
|
||||
|
||||
resp, err := http.Get(baseURL + params.Encode())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse HTML: %v", err)
|
||||
}
|
||||
|
||||
var results []MusicResult
|
||||
|
||||
doc.Find("li.searchresult").Each(func(i int, s *goquery.Selection) {
|
||||
result := MusicResult{Source: "Bandcamp"}
|
||||
|
||||
// URL extraction
|
||||
if urlSel := s.Find("div.itemurl a"); urlSel.Length() > 0 {
|
||||
result.URL = strings.TrimSpace(urlSel.Text())
|
||||
}
|
||||
|
||||
// Title extraction
|
||||
if titleSel := s.Find("div.heading a"); titleSel.Length() > 0 {
|
||||
result.Title = strings.TrimSpace(titleSel.Text())
|
||||
}
|
||||
|
||||
// Artist extraction
|
||||
if artistSel := s.Find("div.subhead"); artistSel.Length() > 0 {
|
||||
result.Artist = strings.TrimSpace(artistSel.Text())
|
||||
}
|
||||
|
||||
// Thumbnail extraction
|
||||
if thumbSel := s.Find("div.art img"); thumbSel.Length() > 0 {
|
||||
result.Thumbnail, _ = thumbSel.Attr("src")
|
||||
}
|
||||
|
||||
// // Iframe URL construction
|
||||
// if linkHref, exists := s.Find("div.itemurl a").Attr("href"); exists {
|
||||
// if itemID := extractSearchItemID(linkHref); itemID != "" {
|
||||
// itemType := strings.ToLower(strings.TrimSpace(s.Find("div.itemtype").Text()))
|
||||
// result.IframeSrc = fmt.Sprintf(
|
||||
// "https://bandcamp.com/EmbeddedPlayer/%s=%s/size=large/bgcol=000/linkcol=fff/artwork=small",
|
||||
// itemType,
|
||||
// itemID,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
results = append(results, result)
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
198
music-soundcloud.go
Normal file
198
music-soundcloud.go
Normal file
|
@ -0,0 +1,198 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
type SoundCloudTrack struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Permalink string `json:"permalink"`
|
||||
ArtworkURL string `json:"artwork_url"`
|
||||
Duration int `json:"duration"`
|
||||
User struct {
|
||||
Username string `json:"username"`
|
||||
Permalink string `json:"permalink"`
|
||||
} `json:"user"`
|
||||
Streams struct {
|
||||
HTTPMP3128URL string `json:"http_mp3_128_url"`
|
||||
} `json:"streams"`
|
||||
}
|
||||
|
||||
func SearchSoundCloud(query string, page int) ([]MusicResult, error) {
|
||||
clientID, err := extractClientID()
|
||||
if err != nil {
|
||||
return searchSoundCloudViaScraping(query, page)
|
||||
}
|
||||
|
||||
apiResults, err := searchSoundCloudViaAPI(query, clientID, page)
|
||||
if err == nil && len(apiResults) > 0 {
|
||||
return convertSoundCloudResults(apiResults), nil
|
||||
}
|
||||
|
||||
return searchSoundCloudViaScraping(query, page)
|
||||
}
|
||||
|
||||
func searchSoundCloudViaAPI(query, clientID string, page int) ([]SoundCloudTrack, error) {
|
||||
const limit = 10
|
||||
offset := (page - 1) * limit
|
||||
|
||||
apiUrl := fmt.Sprintf(
|
||||
"https://api-v2.soundcloud.com/search/tracks?q=%s&client_id=%s&limit=%d&offset=%d",
|
||||
url.QueryEscape(query),
|
||||
clientID,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
|
||||
resp, err := http.Get(apiUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Collection []SoundCloudTrack `json:"collection"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.Collection, nil
|
||||
}
|
||||
|
||||
func convertSoundCloudResults(tracks []SoundCloudTrack) []MusicResult {
|
||||
var results []MusicResult
|
||||
|
||||
for _, track := range tracks {
|
||||
thumbnail := strings.Replace(track.ArtworkURL, "large", "t500x500", 1)
|
||||
trackURL := fmt.Sprintf("https://soundcloud.com/%s/%s",
|
||||
track.User.Permalink,
|
||||
track.Permalink,
|
||||
)
|
||||
|
||||
results = append(results, MusicResult{
|
||||
Title: track.Title,
|
||||
Artist: track.User.Username,
|
||||
URL: trackURL,
|
||||
Thumbnail: thumbnail,
|
||||
//AudioURL: track.Streams.HTTPMP3128URL,
|
||||
Source: "SoundCloud",
|
||||
Duration: fmt.Sprintf("%d", track.Duration/1000),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func searchSoundCloudViaScraping(query string, page int) ([]MusicResult, error) {
|
||||
searchUrl := fmt.Sprintf("https://soundcloud.com/search/sounds?q=%s", url.QueryEscape(query))
|
||||
resp, err := http.Get(searchUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []MusicResult
|
||||
doc.Find("li.searchList__item").Each(func(i int, s *goquery.Selection) {
|
||||
titleElem := s.Find("a.soundTitle__title")
|
||||
artistElem := s.Find("a.soundTitle__username")
|
||||
artworkElem := s.Find(".sound__coverArt")
|
||||
|
||||
title := strings.TrimSpace(titleElem.Text())
|
||||
artist := strings.TrimSpace(artistElem.Text())
|
||||
href, _ := titleElem.Attr("href")
|
||||
thumbnail, _ := artworkElem.Find("span.sc-artwork").Attr("style")
|
||||
|
||||
if thumbnail != "" {
|
||||
if matches := regexp.MustCompile(`url\((.*?)\)`).FindStringSubmatch(thumbnail); len(matches) > 1 {
|
||||
thumbnail = strings.Trim(matches[1], `"`)
|
||||
}
|
||||
}
|
||||
|
||||
if title == "" || href == "" {
|
||||
return
|
||||
}
|
||||
|
||||
trackURL, err := url.Parse(href)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if trackURL.Host == "" {
|
||||
trackURL.Scheme = "https"
|
||||
trackURL.Host = "soundcloud.com"
|
||||
}
|
||||
|
||||
trackURL.Path = strings.ReplaceAll(trackURL.Path, "//", "/")
|
||||
fullURL := trackURL.String()
|
||||
|
||||
results = append(results, MusicResult{
|
||||
Title: title,
|
||||
Artist: artist,
|
||||
URL: fullURL,
|
||||
Thumbnail: thumbnail,
|
||||
Source: "SoundCloud",
|
||||
})
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func extractClientID() (string, error) {
|
||||
resp, err := http.Get("https://soundcloud.com/")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var clientID string
|
||||
doc.Find("script[src]").Each(func(i int, s *goquery.Selection) {
|
||||
if clientID != "" {
|
||||
return
|
||||
}
|
||||
|
||||
src, _ := s.Attr("src")
|
||||
if strings.Contains(src, "sndcdn.com/assets/") {
|
||||
resp, err := http.Get(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
re := regexp.MustCompile(`client_id:"([^"]+)"`)
|
||||
matches := re.FindSubmatch(body)
|
||||
if len(matches) > 1 {
|
||||
clientID = string(matches[1])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if clientID == "" {
|
||||
return "", fmt.Errorf("client_id not found")
|
||||
}
|
||||
return clientID, nil
|
||||
}
|
81
music-spotify.go
Normal file
81
music-spotify.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func SearchSpotify(query string, page int) ([]MusicResult, error) {
|
||||
searchUrl := fmt.Sprintf("https://open.spotify.com/search/%s", url.PathEscape(query))
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", searchUrl, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
// Set user agent ?
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("received non-200 status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse document: %v", err)
|
||||
}
|
||||
|
||||
var results []MusicResult
|
||||
|
||||
// Find track elements
|
||||
doc.Find(`div[data-testid="tracklist-row"]`).Each(func(i int, s *goquery.Selection) {
|
||||
// Extract title
|
||||
title := s.Find(`div[data-testid="tracklist-row__title"] a`).Text()
|
||||
title = strings.TrimSpace(title)
|
||||
|
||||
// Extract artist
|
||||
artist := s.Find(`div[data-testid="tracklist-row__artist"] a`).First().Text()
|
||||
artist = strings.TrimSpace(artist)
|
||||
|
||||
// Extract duration
|
||||
duration := s.Find(`div[data-testid="tracklist-row__duration"]`).First().Text()
|
||||
duration = strings.TrimSpace(duration)
|
||||
|
||||
// Extract URL
|
||||
path, _ := s.Find(`div[data-testid="tracklist-row__title"] a`).Attr("href")
|
||||
fullUrl := fmt.Sprintf("https://open.spotify.com%s", path)
|
||||
|
||||
// Extract thumbnail
|
||||
thumbnail, _ := s.Find(`img[aria-hidden="false"]`).Attr("src")
|
||||
|
||||
if title != "" && artist != "" {
|
||||
results = append(results, MusicResult{
|
||||
Title: title,
|
||||
Artist: artist,
|
||||
URL: fullUrl,
|
||||
Duration: duration,
|
||||
Thumbnail: thumbnail,
|
||||
Source: "Spotify",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
85
music-youtube.go
Normal file
85
music-youtube.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type MusicAPIResponse struct {
|
||||
Items []struct {
|
||||
Title string `json:"title"`
|
||||
UploaderName string `json:"uploaderName"`
|
||||
Duration int `json:"duration"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
URL string `json:"url"`
|
||||
} `json:"items"` // Removed VideoID since we'll parse from URL
|
||||
}
|
||||
|
||||
func SearchMusicViaPiped(query string, page int) ([]MusicResult, error) {
|
||||
var lastError error
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
for _, instance := range pipedInstances {
|
||||
if disabledInstances[instance] {
|
||||
continue
|
||||
}
|
||||
|
||||
url := fmt.Sprintf(
|
||||
"https://%s/search?q=%s&filter=music_songs&page=%d",
|
||||
instance,
|
||||
url.QueryEscape(query),
|
||||
page,
|
||||
)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
printInfo("Disabling instance %s due to error: %v", instance, err)
|
||||
disabledInstances[instance] = true
|
||||
lastError = fmt.Errorf("request to %s failed: %w", instance, err)
|
||||
continue
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
var apiResp MusicAPIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||
lastError = fmt.Errorf("failed to decode response from %s: %w", instance, err)
|
||||
continue
|
||||
}
|
||||
|
||||
return convertPipedToMusicResults(instance, apiResp), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all Piped instances failed, last error: %v", lastError)
|
||||
}
|
||||
|
||||
func convertPipedToMusicResults(instance string, resp MusicAPIResponse) []MusicResult {
|
||||
seen := make(map[string]bool)
|
||||
var results []MusicResult
|
||||
|
||||
for _, item := range resp.Items {
|
||||
// Extract video ID from URL
|
||||
u, err := url.Parse(item.URL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
videoID := u.Query().Get("v")
|
||||
if videoID == "" || seen[videoID] {
|
||||
continue
|
||||
}
|
||||
seen[videoID] = true
|
||||
|
||||
results = append(results, MusicResult{
|
||||
Title: item.Title,
|
||||
Artist: item.UploaderName,
|
||||
URL: fmt.Sprintf("https://music.youtube.com%s", item.URL),
|
||||
Duration: formatDuration(item.Duration),
|
||||
Thumbnail: item.Thumbnail,
|
||||
Source: "YouTube Music",
|
||||
//AudioURL: fmt.Sprintf("https://%s/stream/%s", instance, videoID),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
178
music.go
Normal file
178
music.go
Normal file
|
@ -0,0 +1,178 @@
|
|||
// music.go - Central music search handler
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MusicSearchEngine struct {
|
||||
Name string
|
||||
Func func(query string, page int) ([]MusicResult, error)
|
||||
}
|
||||
|
||||
var (
|
||||
musicSearchEngines []MusicSearchEngine
|
||||
cacheMutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
var allMusicSearchEngines = []MusicSearchEngine{
|
||||
{Name: "SoundCloud", Func: SearchSoundCloud},
|
||||
{Name: "YouTube", Func: SearchMusicViaPiped},
|
||||
{Name: "Bandcamp", Func: SearchBandcamp},
|
||||
//{Name: "Spotify", Func: SearchSpotify},
|
||||
}
|
||||
|
||||
func initMusicEngines() {
|
||||
// Initialize with all engines if no specific config
|
||||
musicSearchEngines = allMusicSearchEngines
|
||||
}
|
||||
|
||||
func handleMusicSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
||||
start := time.Now()
|
||||
|
||||
cacheKey := CacheKey{
|
||||
Query: query,
|
||||
Page: page,
|
||||
Type: "music",
|
||||
Lang: settings.SearchLanguage,
|
||||
Safe: settings.SafeSearch == "active",
|
||||
}
|
||||
|
||||
var results []MusicResult
|
||||
|
||||
if cached, found := resultsCache.Get(cacheKey); found {
|
||||
if musicResults, ok := convertCacheToMusicResults(cached); ok {
|
||||
results = musicResults
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
results = fetchMusicResults(query, page)
|
||||
if len(results) > 0 {
|
||||
resultsCache.Set(cacheKey, convertMusicResultsToCache(results))
|
||||
}
|
||||
}
|
||||
|
||||
go prefetchMusicPages(query, page)
|
||||
|
||||
elapsed := time.Since(start) // Calculate duration
|
||||
fetched := fmt.Sprintf("%.2f %s", elapsed.Seconds(), Translate("seconds"))
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Results": results,
|
||||
"Query": query,
|
||||
"Page": page,
|
||||
"HasPrevPage": page > 1,
|
||||
"HasNextPage": len(results) >= 10, // Default page size
|
||||
"MusicServices": getMusicServiceNames(),
|
||||
"CurrentService": "all", // Default service
|
||||
"Theme": settings.Theme,
|
||||
"IsThemeDark": settings.IsThemeDark,
|
||||
"Trans": Translate,
|
||||
"Fetched": fetched,
|
||||
}
|
||||
|
||||
renderTemplate(w, "music.html", data)
|
||||
}
|
||||
|
||||
// Helper to get music service names
|
||||
func getMusicServiceNames() []string {
|
||||
names := make([]string, len(allMusicSearchEngines))
|
||||
for i, engine := range allMusicSearchEngines {
|
||||
names[i] = engine.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func convertMusicResultsToCache(results []MusicResult) []SearchResult {
|
||||
cacheResults := make([]SearchResult, len(results))
|
||||
for i, r := range results {
|
||||
cacheResults[i] = r
|
||||
}
|
||||
return cacheResults
|
||||
}
|
||||
|
||||
func convertCacheToMusicResults(cached []SearchResult) ([]MusicResult, bool) {
|
||||
results := make([]MusicResult, 0, len(cached))
|
||||
for _, item := range cached {
|
||||
if musicResult, ok := item.(MusicResult); ok {
|
||||
results = append(results, musicResult)
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return results, true
|
||||
}
|
||||
|
||||
func fetchMusicResults(query string, page int) []MusicResult {
|
||||
var results []MusicResult
|
||||
resultsChan := make(chan []MusicResult, len(musicSearchEngines))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, engine := range musicSearchEngines {
|
||||
wg.Add(1)
|
||||
go func(e MusicSearchEngine) {
|
||||
defer wg.Done()
|
||||
res, err := e.Func(query, page)
|
||||
if err == nil && len(res) > 0 {
|
||||
resultsChan <- res
|
||||
}
|
||||
}(engine)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsChan)
|
||||
}()
|
||||
|
||||
for res := range resultsChan {
|
||||
results = append(results, res...)
|
||||
if len(results) >= 50 { // Default max results
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return deduplicateResults(results)
|
||||
}
|
||||
|
||||
func prefetchMusicPages(query string, currentPage int) {
|
||||
for _, page := range []int{currentPage - 1, currentPage + 1} {
|
||||
if page < 1 {
|
||||
continue
|
||||
}
|
||||
cacheKey := CacheKey{
|
||||
Query: query,
|
||||
Page: page,
|
||||
Type: "music",
|
||||
}
|
||||
if _, found := resultsCache.Get(cacheKey); !found {
|
||||
go fetchMusicResults(query, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deduplicateResults(results []MusicResult) []MusicResult {
|
||||
seen := make(map[string]bool)
|
||||
var unique []MusicResult
|
||||
|
||||
for _, res := range results {
|
||||
if !seen[res.URL] {
|
||||
seen[res.URL] = true
|
||||
unique = append(unique, res)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
// func generatePlayerHTML(result MusicResult) template.HTML {
|
||||
// if result.IframeSrc != "" {
|
||||
// return template.HTML(fmt.Sprintf(
|
||||
// `<iframe width="100%%" height="166" scrolling="no" frameborder="no" src="%s"></iframe>`,
|
||||
// result.IframeSrc,
|
||||
// ))
|
||||
// }
|
||||
// return template.HTML("")
|
||||
// }
|
119
static/css/style-music.css
Normal file
119
static/css/style-music.css
Normal file
|
@ -0,0 +1,119 @@
|
|||
/* Music Results Styling */
|
||||
.result-item.music-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.music-thumbnail {
|
||||
position: relative;
|
||||
flex: 0 0 160px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: var(--placeholder-bg);
|
||||
}
|
||||
|
||||
.music-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.music-thumbnail:hover img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.thumbnail-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--placeholder-bg);
|
||||
color: var(--placeholder-icon);
|
||||
}
|
||||
|
||||
.thumbnail-placeholder .material-icons-round {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.duration-overlay {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.music-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.music-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.music-title:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.music-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.artist {
|
||||
color: var(--accent-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meta-separator {
|
||||
color: var(--border-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.music-thumbnail {
|
||||
flex-basis: 120px;
|
||||
}
|
||||
|
||||
.music-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.music-meta {
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.music-thumbnail {
|
||||
flex-basis: 100px;
|
||||
}
|
||||
|
||||
.duration-overlay {
|
||||
font-size: 11px;
|
||||
padding: 3px 6px;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
BIN
static/fonts/MaterialIcons-Round.woff2
Normal file
BIN
static/fonts/MaterialIcons-Round.woff2
Normal file
Binary file not shown.
Binary file not shown.
|
@ -103,6 +103,10 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button> <!-- Video icon -->
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button> <!-- Forum icon -->
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
|
|
|
@ -103,6 +103,10 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button> <!-- Video icon -->
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable search-active" name="t" value="forum"></button> <!-- Forum icon -->
|
||||
<button name="t" value="forum" class="clickable search-active">{{ translate "forums" }}</button>
|
||||
|
|
|
@ -113,6 +113,10 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button>
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button>
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
|
|
|
@ -118,6 +118,10 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button> <!-- Movie icon -->
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button> <!-- Forum icon -->
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
|
|
191
templates/music.html
Normal file
191
templates/music.html
Normal file
|
@ -0,0 +1,191 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{ .Query }} - Music Search - {{ translate "site_name" }}</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
<link rel="stylesheet" href="/static/css/style-fonts.css">
|
||||
<link rel="stylesheet" href="/static/css/style-music.css">
|
||||
<link rel="stylesheet" href="/static/css/style-menu.css">
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="{{ translate "site_name" }}" href="/opensearch.xml">
|
||||
<!-- Icons -->
|
||||
<link rel="icon" href="{{ .IconPathSVG }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ .IconPathPNG }}" type="image/png">
|
||||
<link rel="apple-touch-icon" href="{{ .IconPathPNG }}">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Menu Button -->
|
||||
<div id="content" class="js-enabled">
|
||||
<div class="settings-search-div settings-search-div-search">
|
||||
<button class="material-icons-round clickable settings-icon-link settings-icon-link-search"></button>
|
||||
</div>
|
||||
<div class="search-menu settings-menu-hidden">
|
||||
<h2>Settings</h2>
|
||||
<div class="settings-content">
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">All settings</button>
|
||||
<div class="theme-settings">
|
||||
<p><span class="highlight">Current theme: </span> <span id="theme_name">{{.Theme}}</span></p>
|
||||
<div class="themes-settings-menu">
|
||||
<div><img class="view-image-search clickable" id="dark_theme" alt="Dark Theme" src="/static/images/dark.webp"></div>
|
||||
<div><img class="view-image-search clickable" id="light_theme" alt="Light Theme" src="/static/images/light.webp"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="lang" name="safe" id="safeSearchSelect">
|
||||
<option value="disabled" {{if eq .Safe "disabled"}}selected{{end}}>Safe Search Off</option>
|
||||
<option value="active" {{if eq .Safe "active"}}selected{{end}}>Safe Search On</option>
|
||||
</select>
|
||||
<select class="lang" name="lang" id="languageSelect">
|
||||
{{range .LanguageOptions}}
|
||||
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button id="aboutQGatoBtn">About QGato</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<noscript>
|
||||
<div class="settings-search-div settings-search-div-search">
|
||||
<a href="/settings" class="material-icons-round clickable settings-icon-link settings-icon-link-search"></a>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<!-- Popup Modal for QGato -->
|
||||
<div id="aboutQGatoModal">
|
||||
<!-- Close Button -->
|
||||
<button class="btn-nostyle" id="close-button">
|
||||
<div class="material-icons-round icon_visibility clickable cloase-btn"></div>
|
||||
</button>
|
||||
|
||||
<div class="modal-content">
|
||||
<img
|
||||
src="/static/images/icon.svg"
|
||||
alt="QGato"
|
||||
>
|
||||
<h2>QGato</h2>
|
||||
<p>A open-source private search engine.</p>
|
||||
<div class="button-container">
|
||||
<button onclick="window.location.href='https://weforge.xyz/Spitfire/Search'">Source Code</button>
|
||||
<button onclick="window.location.href='/privacy'">Privacy policy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="/music" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
||||
<h1 class="logomobile">
|
||||
<div class="logo-container" href="/">
|
||||
<a href="/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="86" viewBox="0 0 29 86"><path fill-rule="evenodd" d="M-44.35.78C-70.8 6.76-74.8 43.17-50.67 55.73c1.7.88 4.42 1.7 7.83 2.22 4.48.68 9.6.86 9.58.15-.04-1.43-7.3-8.28-8.67-8.28-3.15 0-9.94-5.66-11.97-10C-61.95 22.66-48.1 4.54-31.12 10c13.5 4.34 18.1 22.7 8.66 34.44-1.85 2.3-1.75 2.3-4.4-.22-4.8-4.59-8.57-5.25-11.98-2.1-2.18 2-2.15 2.66.15 4.14 1.9 1.22 13.4 12.95 17.49 17.83 4.3 5.13 5.24 6.14 7.52 7.97C-9.25 75.6-1.23 77.91 1 76.28c.67-.5 1.86-7.8 1.35-8.3-.12-.12-1.34-.4-2.7-.61-5.36-.86-9.23-3.46-14.2-9.55-3.49-4.27-4.12-5.26-3.38-5.26 2.54 0 8.05-8.62 9.86-15.43C-2.36 15.63-22.18-4.23-44.35.78m65.13 1.53C4.92 6.02-4.86 22.36-.72 38.24 3 52.62 18.43 59.63 33.67 57.64c4.7-.62 2.43-.66 4.45-.8s6.45-.01 6.93-.2c.4.03.72-.45.72-.94V42.31c0-7.36-.16-13.62-.33-13.9-.26-.4-2.36-.49-10.19-.4-11.44.15-10.96-.03-10.96 4.09 0 2.44-.04 3.99 1.17 4.7 1.13.68 3.43.59 6.68.41l3.76-.2.27 5.68c.33 6.59.57 6.15-3.64 6.7-15.53 2.04-24-5.02-23.37-19.43.66-15.1 12.2-22.78 26.96-17.94 4.5 1.47 4.4 1.52 6.16-2.8 1.5-3.68 1.5-3.69-.82-4.69C36.03 2.2 25.9 1.11 20.78 2.31m78.83.8c-2.87.76-2.9.84-3.15 6.12-.25 5.56.12 4.96-3.35 5.29-3.43.32-3.32.15-2.76 4.2.61 4.37.6 4.34 3.76 4.34h2.65v12.7c0 14.5 1.55 16.33 3.5 18.3 3.6 3.48 9.59 4.92 14.93 3.06 2.45-.85 2.43-.8 2.18-4.95-.25-4.1-.43-3.5-3.16-2.91-7.73 1.64-8.27.6-8.27-15.05V22.87h5.66l5.34-.1c.67-.01.97.4 1.28-3.9.35-4.8-.2-4.01-.8-4.14l-5.82.18-5.66.26v-5.16c0-5.84-.2-6.48-2.25-7.04-1.75-.49-1.76-.49-4.08.13m-34.5 11.02c-2.64.38-4.71 1.04-8.54 2.72l-4.03 1.76c-1.09.39-.28 1.29.69 3.89 1.06 2.75 1.35 3.35 2.11 3.03.76-.32.7-.23 1.43-.65 9.08-5.25 20.26-2.63 20.26 4.74v2.14l-5.95.2c-13.84.48-20.29 4.75-20.38 13.51-.13 12.4 14.18 17.22 24.62 8.3l2.3-1.97.23 1.85c.32 2.53.6 3.06 2.04 3.67 1.42.6 7.16.62 7.75.03.77-.77.37-6-.25-6.34-.94-.5-.77-1.57-.88-12.63-.15-14.87-.5-16.5-4.4-20.13-3.03-2.84-11.55-4.9-17-4.12m72.86 0c-27.2 5.27-24.13 43.96 3.47 43.9 14.67-.04 24.4-12.77 21.53-28.16-1.86-9.95-14.33-17.8-25-15.73m8.29 8.96c6.88 2.34 9.61 11.51 5.9 19.79-4.13 9.19-17.89 9.17-22.14-.03-1.32-2.85-1.24-10.79.14-13.54 3-6 9.45-8.49 16.1-6.22m-68.84 18.5v3.09l-1.85 1.63c-7.46 6.58-16.36 5.49-15.6-1.9.45-4.35 3.62-5.77 13.06-5.87l4.4-.05v3.1" style="fill:currentColor" transform="translate(-31.68 4.9)"/><path d="M-13.47 73.3v1.11q-.65-.3-1.23-.46-.57-.15-1.11-.15-.93 0-1.44.36-.5.36-.5 1.03 0 .56.33.85.34.28 1.28.46l.69.14q1.27.24 1.88.86.6.6.6 1.64 0 1.22-.82 1.86-.82.63-2.4.63-.6 0-1.28-.14-.68-.13-1.4-.4v-1.17q.7.39 1.36.58.67.2 1.31.2.98 0 1.51-.38.54-.39.54-1.1 0-.62-.39-.97-.38-.35-1.25-.53l-.7-.13q-1.27-.26-1.84-.8-.57-.54-.57-1.51 0-1.12.78-1.76.8-.65 2.18-.65.6 0 1.2.1.63.12 1.27.33zm2.29-.28h5.34V74h-4.2v2.5h4.02v.96h-4.02v3.05h4.3v.97h-5.44zm10.14 1.13-1.55 4.2H.5zm-.65-1.13h1.3l3.21 8.45H1.64L.87 79.3h-3.8l-.78 2.17h-1.2zm9.75 4.48q.37.13.71.54.35.4.7 1.12l1.16 2.3H9.41L8.33 79.3q-.42-.85-.82-1.13-.39-.27-1.07-.27H5.2v3.57H4.06v-8.45h2.58q1.44 0 2.16.6.7.61.7 1.84 0 .8-.36 1.32-.37.52-1.08.73zM5.2 73.97v3h1.44q.82 0 1.24-.38.42-.38.42-1.12 0-.75-.42-1.12-.42-.38-1.24-.38zm12.65-.3v1.2q-.58-.53-1.23-.8-.65-.26-1.39-.26-1.45 0-2.22.89-.77.88-.77 2.55t.77 2.56q.77.88 2.22.88.74 0 1.39-.26.65-.27 1.23-.8v1.19q-.6.4-1.27.6-.67.21-1.42.21-1.91 0-3.02-1.17-1.1-1.18-1.1-3.2 0-2.04 1.1-3.21 1.1-1.18 3.02-1.18.76 0 1.43.2.67.2 1.26.6zm1.76-.65h1.14v3.46h4.15v-3.46h1.15v8.45H24.9v-4.02h-4.15v4.02h-1.14zm12.39 0h5.34V74h-4.2v2.5h4.02v.96h-4.02v3.05h4.3v.97H32zm7.32 0h1.53l3.75 7.07v-7.07h1.1v8.45h-1.53l-3.74-7.07v7.07h-1.11zm14.42 7.24V78h-1.87v-.93h3v3.62q-.67.47-1.46.71-.8.24-1.7.24-1.98 0-3.1-1.15-1.12-1.16-1.12-3.23 0-2.07 1.12-3.22 1.12-1.16 3.1-1.16.82 0 1.56.2.75.2 1.38.6v1.22q-.64-.54-1.35-.8-.71-.28-1.5-.28-1.55 0-2.33.86-.77.87-.77 2.58t.77 2.58q.78.86 2.33.86.6 0 1.08-.1.48-.1.86-.33zm3.21-7.24h1.14v8.45h-1.14zm3.42 0h1.54l3.74 7.07v-7.07h1.1v8.45h-1.53l-3.74-7.07v7.07h-1.11zm8.66 0h5.34V74h-4.2v2.5h4.02v.96h-4.02v3.05h4.3v.97h-5.44z" aria-label="SEARCH ENGINE" style="font-family:'ADLaM Display';white-space:pre;fill:currentColor"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</h1>
|
||||
<div class="wrapper-results">
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="music"></button>
|
||||
<div class="autocomplete">
|
||||
<ul></ul>
|
||||
</div>
|
||||
<input type="submit" class="hide" name="t" value="music" />
|
||||
</div>
|
||||
<div class="sub-search-button-wrapper">
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="text"></button>
|
||||
<button name="t" value="text" class="clickable">{{ translate "web" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="image"></button>
|
||||
<button name="t" value="image" class="clickable">{{ translate "images" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button>
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable search-active" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable search-active">{{ translate "music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button>
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
</div>
|
||||
<div id="content2" class="js-enabled">
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="map"></button>
|
||||
<button name="t" value="map" class="clickable">{{ translate "maps" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="file"></button>
|
||||
<button name="t" value="file" class="clickable">{{ translate "torrents" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="results_settings" action="/music" method="get">
|
||||
</form>
|
||||
<p class="fetched fetched_dif_files">{{ translate "fetched_in" .Fetched }}</p>
|
||||
|
||||
<div class="results" id="results">
|
||||
{{if .Results}}
|
||||
{{range .Results}}
|
||||
<div class="result-item music-item">
|
||||
<div class="music-thumbnail">
|
||||
<a href="{{.URL}}">
|
||||
<img src="{{.Thumbnail}}" alt="{{.Title}} thumbnail" loading="lazy">
|
||||
{{if .Duration}}<div class="duration-overlay">{{.Duration}}</div>{{end}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="music-info">
|
||||
<a href="{{.URL}}"><h3 class="video_title">{{.Title}}</h3></a>
|
||||
<div class="stats">
|
||||
<span class="artist">{{.Artist}}</span>
|
||||
<span class="pipe">|</span>
|
||||
<span class="source">{{.Source}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else if .NoResults}}
|
||||
<div class="no-results-found">
|
||||
{{ translate "no_results_found" .Query }}<br>
|
||||
{{ translate "suggest_rephrase" }}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="no-results-found">{{ translate "no_more_results" }}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="message-bottom-left" id="message-bottom-left">
|
||||
<span>{{ translate "searching_for_new_results" }}</span>
|
||||
</div>
|
||||
<div class="prev-next prev-img" id="prev-next">
|
||||
<form action="/music" method="get">
|
||||
<input type="hidden" name="q" value="{{ .Query }}">
|
||||
<input type="hidden" name="t" value="music">
|
||||
<noscript>
|
||||
{{ if .HasPrevPage }}
|
||||
<button type="submit" name="p" value="{{ sub .Page 1 }}">{{ translate "previous" }}</button>
|
||||
{{ end }}
|
||||
{{ if .HasNextPage }}
|
||||
<button type="submit" name="p" value="{{ add .Page 1 }}">{{ translate "next" }}</button>
|
||||
{{ end }}
|
||||
</noscript>
|
||||
</form>
|
||||
</div>
|
||||
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="music"></div>
|
||||
<script defer src="/static/js/dynamicscrolling.js"></script>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script defer src="/static/js/minimenu.js"></script>
|
||||
<script>
|
||||
document.querySelectorAll('.js-enabled').forEach(el => el.classList.remove('js-enabled'));
|
||||
|
||||
// Handle music service selection
|
||||
document.getElementById('musicServiceSelect').addEventListener('change', function() {
|
||||
const form = this.closest('form');
|
||||
form.submit();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -121,6 +121,13 @@
|
|||
<p>{{ translate "videos" }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico-video" class="material-icons-round clickable" name="t" value="music">
|
||||
<span class="material-icons-round"></span> <!-- 'note' icon -->
|
||||
<p>{{ translate "music" }}</p>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="icon-button">
|
||||
<button id="sub-search-wrapper-ico-forum" class="material-icons-round clickable" name="t" value="forum">
|
||||
|
|
|
@ -104,6 +104,10 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button>
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button>
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
|
|
|
@ -103,6 +103,10 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable search-active" name="t" value="video"></button>
|
||||
<button name="t" value="video" class="clickable search-active">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button>
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
|
|
2
text.go
2
text.go
|
@ -92,7 +92,7 @@ func getTextResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string,
|
|||
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
|
||||
}
|
||||
} else {
|
||||
textResults, _, _, _ := convertToSpecificResults(results)
|
||||
textResults, _, _, _, _ := convertToSpecificResults(results)
|
||||
combinedResults = textResults
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue