diff --git a/cache.go b/cache.go index 5ba863a..f769066 100644 --- a/cache.go +++ b/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 } diff --git a/files.go b/files.go index d2b4837..a93710d 100755 --- a/files.go +++ b/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): diff --git a/images.go b/images.go index 52d2e67..cc4d77e 100755 --- a/images.go +++ b/images.go @@ -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): diff --git a/init.go b/init.go index 6a079db..87dc0ce 100644 --- a/init.go +++ b/init.go @@ -81,6 +81,7 @@ func main() { initImageEngines() initFileEngines() initPipedInstances() + initMusicEngines() } InitializeLanguage("en") // Initialize language before generating OpenSearch diff --git a/lang/en/LC_MESSAGES/default.po b/lang/en/LC_MESSAGES/default.po index eb0843d..c146dac 100644 --- a/lang/en/LC_MESSAGES/default.po +++ b/lang/en/LC_MESSAGES/default.po @@ -88,6 +88,9 @@ msgstr "Video" msgid "videos" msgstr "Videos" +msgid "music" +msgstr "Music" + msgid "forum" msgstr "Forum" diff --git a/main.go b/main.go index c1b769d..5038b6c 100755 --- a/main.go +++ b/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": diff --git a/music-bandcamp.go b/music-bandcamp.go new file mode 100644 index 0000000..2c3210f --- /dev/null +++ b/music-bandcamp.go @@ -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 +} diff --git a/music-soundcloud.go b/music-soundcloud.go new file mode 100644 index 0000000..f8a7221 --- /dev/null +++ b/music-soundcloud.go @@ -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 +} diff --git a/music-spotify.go b/music-spotify.go new file mode 100644 index 0000000..d33e6a3 --- /dev/null +++ b/music-spotify.go @@ -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 +} diff --git a/music-youtube.go b/music-youtube.go new file mode 100644 index 0000000..698dc71 --- /dev/null +++ b/music-youtube.go @@ -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 +} diff --git a/music.go b/music.go new file mode 100644 index 0000000..34dd70d --- /dev/null +++ b/music.go @@ -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( +// ``, +// result.IframeSrc, +// )) +// } +// return template.HTML("") +// } diff --git a/static/css/style-music.css b/static/css/style-music.css new file mode 100644 index 0000000..fccc9cd --- /dev/null +++ b/static/css/style-music.css @@ -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; + } +} \ No newline at end of file diff --git a/static/fonts/MaterialIcons-Round.woff2 b/static/fonts/MaterialIcons-Round.woff2 new file mode 100644 index 0000000..f94dba5 Binary files /dev/null and b/static/fonts/MaterialIcons-Round.woff2 differ diff --git a/static/fonts/material-icons-round-v108-latin-regular.woff2 b/static/fonts/material-icons-round-v108-latin-regular.woff2 index 6f6a973..c143837 100644 Binary files a/static/fonts/material-icons-round-v108-latin-regular.woff2 and b/static/fonts/material-icons-round-v108-latin-regular.woff2 differ diff --git a/templates/files.html b/templates/files.html index 7e39e29..94fc0c6 100755 --- a/templates/files.html +++ b/templates/files.html @@ -103,6 +103,10 @@ +
+ + +
diff --git a/templates/forums.html b/templates/forums.html index a8015cd..c8400bb 100755 --- a/templates/forums.html +++ b/templates/forums.html @@ -103,6 +103,10 @@
+
+ + +
diff --git a/templates/images.html b/templates/images.html index d58f015..5a44b0b 100755 --- a/templates/images.html +++ b/templates/images.html @@ -113,6 +113,10 @@
+
+ + +
diff --git a/templates/map.html b/templates/map.html index 054f910..3d6e5a3 100644 --- a/templates/map.html +++ b/templates/map.html @@ -118,6 +118,10 @@
+
+ + +
diff --git a/templates/music.html b/templates/music.html new file mode 100644 index 0000000..2ac7198 --- /dev/null +++ b/templates/music.html @@ -0,0 +1,191 @@ + + + + + + {{ if .IsThemeDark }} + + {{ end }} + {{ .Query }} - Music Search - {{ translate "site_name" }} + + + + + + + + + + + + + +
+ +
+

Settings

+
+ +
+

Current theme: {{.Theme}}

+
+
Dark Theme
+
Light Theme
+
+
+ + + +
+
+
+ + + +
+ + + + +
+ +
+

+
+ + + +
+

+
+ + +
+
    +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    +
    +
    +

    {{ translate "fetched_in" .Fetched }}

    + +
    + {{if .Results}} + {{range .Results}} +
    + +
    +

    {{.Title}}

    +
    + {{.Artist}} + | + {{.Source}} +
    +
    +
    + {{end}} + {{else if .NoResults}} +
    + {{ translate "no_results_found" .Query }}
    + {{ translate "suggest_rephrase" }} +
    + {{else}} +
    {{ translate "no_more_results" }}
    + {{end}} +
    +
    + {{ translate "searching_for_new_results" }} +
    +
    +
    + + + +
    +
    +
    + + + + + + \ No newline at end of file diff --git a/templates/search.html b/templates/search.html index 44445fe..a6e0b12 100755 --- a/templates/search.html +++ b/templates/search.html @@ -121,6 +121,13 @@

    {{ translate "videos" }}

    + +
    + +
    +
    + + +
    diff --git a/templates/videos.html b/templates/videos.html index 1cda0b7..a011b2c 100644 --- a/templates/videos.html +++ b/templates/videos.html @@ -103,6 +103,10 @@
    +
    + + +
    diff --git a/text.go b/text.go index b59d8bb..3422f84 100755 --- a/text.go +++ b/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):