<commit_before>// Package gmusic provides methods to list and play music from Google
// Play Music.
package gmusic

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"strings"
	"time"

	"github.com/mjibson/gpsoauth"
)

const (
	googlePlayMusicEndpoint = "https://play.google.com/music"
	serviceName             = "sj"
	sjURL                   = "https://www.googleapis.com/sj/v1.1/"
)

type GMusic struct {
	DeviceID string
	Auth     string
}

// Login logs in with a username and password and androidID from a MAC
// address of the machine.
func Login(username, password string) (*GMusic, error) {
	return LoginAndroid(username, password, gpsoauth.GetNode())
}

// LoginAndroid logs in with a username and password and given androidID.
func LoginAndroid(username, password, androidID string) (*GMusic, error) {
	auth, err := gpsoauth.Login(username, password, androidID, serviceName)
	if err != nil {
		return nil, err
	}
	gm := GMusic{
		Auth: auth,
	}
	if err := gm.setDeviceID(); err != nil {
		return nil, err
	}
	return &gm, nil
}

func (g *GMusic) request(method, url string, data interface{}, client *http.Client) (*http.Response, error) {
	var body io.Reader
	if data != nil {
		buf := new(bytes.Buffer)
		if err := json.NewEncoder(buf).Encode(data); err != nil {
			return nil, err
		}
		body = buf
	}
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Add("Authorization", fmt.Sprintf("GoogleLogin auth=%s", g.Auth))
	req.Header.Add("Content-Type", "application/json")
	if client == nil {
		client = http.DefaultClient
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf("gmusic: %s", resp.Status)
	}
	return resp, nil
}

func (g *GMusic) sjRequest(method, path string, data interface{}) (*http.Response, error) {
	return g.request(method, sjURL+path, data, nil)
}

func (g *GMusic) setDeviceID() error {
	req, err := http.NewRequest("HEAD", googlePlayMusicEndpoint+"/listen", nil)
	if err != nil {
		return err
	}
	req.Header.Add("Authorization", "GoogleLogin auth="+g.Auth)
	jar, _ := cookiejar.New(nil)
	client := &http.Client{
		Jar: jar,
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	resp.Body.Close()
	xt := make(url.Values)
	u, _ := url.Parse(googlePlayMusicEndpoint)
	for _, c := range jar.Cookies(u) {
		if c.Name == "xt" {
			xt.Set("xt", c.Value)
		}
	}
	settings, err := g.settings(xt, client)
	if err != nil {
		return err
	}
	for _, d := range settings.Devices {
		if d.Type != "PHONE" || len(d.ID) != 18 {
			continue
		}
		g.DeviceID = d.ID[2:]
		break
	}
	if g.DeviceID == "" {
		return fmt.Errorf("no valid devices")
	}
	return nil
}

func (g *GMusic) settings(xtData url.Values, jarClient *http.Client) (*Settings, error) {
	resp, err := g.request("POST", googlePlayMusicEndpoint+"/services/loadsettings?"+xtData.Encode(), nil, jarClient)
	if err != nil {
		return nil, err
	}
	var data SettingsData
	defer resp.Body.Close()
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		return nil, err
	}
	return &data.Settings, nil
}

type SettingsData struct {
	Settings Settings `json:"settings"`
}

type Settings struct {
	Devices []struct {
		Carrier      string `json:"carrier"`
		Date         int    `json:"date"`
		ID           string `json:"id"`
		LastUsedMs   int    `json:"lastUsedMs"`
		Manufacturer string `json:"manufacturer"`
		Model        string `json:"model"`
		Name         string `json:"name"`
		Type         string `json:"type"`
	} `json:"devices"`
	ExpirationMillis int  `json:"expirationMillis"`
	IsCanceled       bool `json:"isCanceled"`
	IsSubscription   bool `json:"isSubscription"`
	IsTrial          bool `json:"isTrial"`
	Labs             []struct {
		Description string `json:"description"`
		Enabled     bool   `json:"enabled"`
		Name        string `json:"name"`
		Title       string `json:"title"`
	} `json:"labs"`
	MaxTracks              int  `json:"maxTracks"`
	SubscriptionNewsletter bool `json:"subscriptionNewsletter"`
}<commit_msg>Use new settings route<commit_after>// Package gmusic provides methods to list and play music from Google
// Play Music.
package gmusic

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"strings"
	"time"

	"github.com/mjibson/gpsoauth"
)

const (
	googlePlayMusicEndpoint = "https://play.google.com/music"
	serviceName             = "sj"
	sjURL                   = "https://www.googleapis.com/sj/v1.1/"
)

type GMusic struct {
	DeviceID string
	Auth     string
}

// Login logs in with a username and password and androidID from a MAC
// address of the machine.
func Login(username, password string) (*GMusic, error) {
	return LoginAndroid(username, password, gpsoauth.GetNode())
}

// LoginAndroid logs in with a username and password and given androidID.
func LoginAndroid(username, password, androidID string) (*GMusic, error) {
	auth, err := gpsoauth.Login(username, password, androidID, serviceName)
	if err != nil {
		return nil, err
	}
	gm := GMusic{
		Auth: auth,
	}
	if err := gm.setDeviceID(); err != nil {
		return nil, err
	}
	return &gm, nil
}

func (g *GMusic) request(method, url string, data interface{}, client *http.Client) (*http.Response, error) {
	var body io.Reader
	if data != nil {
		buf := new(bytes.Buffer)
		if err := json.NewEncoder(buf).Encode(data); err != nil {
			return nil, err
		}
		body = buf
	}
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Add("Authorization", fmt.Sprintf("GoogleLogin auth=%s", g.Auth))
	req.Header.Add("Content-Type", "application/json")
	if client == nil {
		client = http.DefaultClient
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK {
		resp.Body.Close()
		return nil, fmt.Errorf("gmusic: %s", resp.Status)
	}
	return resp, nil
}

func (g *GMusic) sjRequest(method, path string, data interface{}) (*http.Response, error) {
	return g.request(method, sjURL+path, data, nil)
}

func (g *GMusic) setDeviceID() error {
	const phoneDevice = 2
	req, err := http.NewRequest("HEAD", googlePlayMusicEndpoint+"/listen", nil)
	if err != nil {
		return err
	}
	req.Header.Add("Authorization", "GoogleLogin auth="+g.Auth)
	jar, _ := cookiejar.New(nil)
	client := &http.Client{
		Jar: jar,
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	resp.Body.Close()
	xt := make(url.Values)
	u, _ := url.Parse(googlePlayMusicEndpoint)
	for _, c := range jar.Cookies(u) {
		if c.Name == "xt" {
			xt.Set("xt", c.Value)
		}
	}
	settings, err := g.settings(xt, client)
	if err != nil {
		return err
	}
	for _, d := range settings.UploadDevice {
		if d.DeviceType != phoneDevice || len(d.ID) != 18 {
			continue
		}
		g.DeviceID = d.ID[2:]
		break
	}
	if g.DeviceID == "" {
		return fmt.Errorf("no valid devices")
	}
	return nil
}

func (g *GMusic) settings(xtData url.Values, jarClient *http.Client) (*Settings, error) {
	resp, err := g.request("POST", googlePlayMusicEndpoint+"/services/fetchsettings?"+xtData.Encode(), nil, jarClient)
	if err != nil {
		return nil, err
	}
	var data SettingsData
	defer resp.Body.Close()
	if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
		return nil, err
	}
	return &data.Settings, nil
}

type SettingsData struct {
	Settings Settings `json:"settings"`
}

type Settings struct {
	EntitlementInfo struct {
		ExpirationMillis int  `json:"expirationMillis"`
		IsCanceled       bool `json:"isCanceled"`
		IsSubscription   bool `json:"isSubscription"`
		IsTrial          bool `json:"isTrial"`
	} `json:"entitlementInfo"`
	Lab []struct {
		Description    string `json:"description"`
		DisplayName    string `json:"displayName"`
		Enabled        bool   `json:"enabled"`
		ExperimentName string `json:"experimentName"`
	} `json:"lab"`
	MaxUploadedTracks      int  `json:"maxUploadedTracks"`
	SubscriptionNewsletter bool `json:"subscriptionNewsletter"`
	UploadDevice           []struct {
		DeviceType             int    `json:"deviceType"`
		ID                     string `json:"id"`
		LastAccessedFormatted  string `json:"lastAccessedFormatted"`
		LastAccessedTimeMillis int    `json:"lastAccessedTimeMillis"`
		LastEventTimeMillis    int    `json:"lastEventTimeMillis"`
		Name                   string `json:"name"`
	} `json:"uploadDevice"`
}
{'fraction_non_alphanumeric': 0.14409967089797837, 'fraction_numerical': 0.001998119417019276, 'mean_word_length': 3.5478353821485835, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4453792', 'n_tokens_mistral': 3031, 'n_tokens_neox': 2921, 'n_words': 775}