package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
var httpClient *http.Client
func initHTTPClient() {
httpClient = &http.Client{Timeout: time.Duration(cfg.HTTPTimeout) * time.Second}
}
func postInnerTube(endpoint string, extraFields map[string]any) ([]byte, error) {
body := map[string]any{
"context": map[string]any{
"client": map[string]any{
"clientName": "WEB_REMIX",
"clientVersion": "1.20260225.01.00",
},
},
}
for k, v := range extraFields {
body[k] = v
}
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
url := InnerTubeBase + "/" + endpoint + "?prettyPrint=false"
resp, err := httpClient.Post(url, "application/json", bytes.NewReader(data))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d from %s", resp.StatusCode, endpoint)
}
return io.ReadAll(resp.Body)
}
func SearchArtists(query string) ([]Artist, error) {
data, err := postInnerTube("search", map[string]any{
"query": query,
"params": "EgWKAQIIAWoKEAMQBBAJEAoQBQ==",
})
if err != nil {
return nil, err
}
ids := reChannelID.FindAllString(string(data), -1)
seen := make(map[string]bool)
var unique []string
for _, id := range ids {
if !seen[id] {
seen[id] = true
unique = append(unique, id)
}
if len(unique) >= cfg.MaxArtistResults {
break
}
}
var artists []Artist
for _, id := range unique {
name, mpreIDs, err := BrowseChannel(id)
if err != nil {
continue
}
if len(mpreIDs) == 0 {
continue
}
artists = append(artists, Artist{
ID: id,
Name: name,
ReleaseCount: len(mpreIDs),
})
}
return artists, nil
}
func BrowseChannel(channelID string) (string, []string, error) {
data, err := postInnerTube("browse", map[string]any{
"browseId": channelID,
})
if err != nil {
return "", nil, err
}
text := string(data)
name := ""
if loc := reHeaderTag.FindStringIndex(text); loc != nil {
// Extract first "text":"..." after musicImmersiveHeaderRenderer
if m := reTitle.FindStringSubmatch(text[loc[1]:]); m != nil {
name = strings.TrimSpace(m[1])
}
}
if name == "" {
// Fallback to first text match
if m := reTitle.FindStringSubmatch(text); m != nil {
name = m[1]
}
}
mpreIDs := reMPREID.FindAllString(text, -1)
seen := make(map[string]bool)
var unique []string
for _, id := range mpreIDs {
if !seen[id] {
seen[id] = true
unique = append(unique, id)
}
}
return name, unique, nil
}
func ResolveMPRE(mpreID string) (Release, error) {
data, err := postInnerTube("browse", map[string]any{
"browseId": mpreID,
})
if err != nil {
return Release{}, err
}
text := string(data)
olakID := ""
if m := reOLAKID.FindString(text); m != "" {
olakID = m
}
if olakID == "" {
return Release{}, fmt.Errorf("no OLAK ID found for %s", mpreID)
}
albumTitle := ""
if m := reTitle.FindStringSubmatch(text); m != nil {
albumTitle = m[1]
}
artist := ""
if m := reStrapline.FindStringSubmatch(text); m != nil {
artist = strings.TrimSpace(m[1])
}
return Release{
MPREID: mpreID,
OLAKID: olakID,
Title: albumTitle,
Artist: artist,
}, nil
}
func SearchAlbums(query string) ([]Release, error) {
data, err := postInnerTube("search", map[string]any{
"query": query,
"params": "EgWKAQIYAWoKEAMQBBAJEAoQBQ==",
})
if err != nil {
return nil, err
}
ids := reMPREID.FindAllString(string(data), -1)
seen := make(map[string]bool)
var unique []string
for _, id := range ids {
if !seen[id] {
seen[id] = true
unique = append(unique, id)
}
if len(unique) >= cfg.MaxAlbumResults {
break
}
}
return ResolveAllReleases(unique), nil
}
func ResolveAllReleases(mpreIDs []string) []Release {
var releases []Release
for _, id := range mpreIDs {
r, err := ResolveMPRE(id)
if err != nil {
continue
}
releases = append(releases, r)
}
return releases
}