mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 09:06:14 +00:00
Enhance account overview UI and make fields editable (#127)
- Added detailed provider settings display to account overview - Made 'Language' field editable with auto-save functionality (currently only `en` and `de` available without actual effect on any UI or speaker config) - Made 'SPOTIFY - STREAMING_QUALITY' editable with descriptive quality options - ⚠️ this currently only writes the account config, but does not update the actual speaker setting - Improved account data persistence and error handling - Added tests for new management API endpoints and data store changes --------- Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
co-authored by
Junie
parent
9f7cb81b45
commit
61b5c71097
@@ -757,6 +757,8 @@ func setupRouter(server *handlers.Server) *chi.Mux {
|
||||
r.Route("/accounts", func(r chi.Router) {
|
||||
r.Get("/", server.HandleMgmtListAccounts)
|
||||
r.Get("/{accountId}", server.HandleMgmtAccountDetails)
|
||||
r.Post("/{accountId}/language", server.HandleMgmtUpdateAccountLanguage)
|
||||
r.Post("/{accountId}/provider-settings", server.HandleMgmtUpdateAccountProviderSetting)
|
||||
r.Get("/{accountId}/speakers", server.HandleMgmtListSpeakers)
|
||||
})
|
||||
|
||||
|
||||
@@ -439,6 +439,7 @@ type ServiceAccountInfo struct {
|
||||
AccountID string `json:"account_id"`
|
||||
PreferredLanguage string `json:"preferred_language"`
|
||||
ProviderSettings []ProviderSetting `json:"provider_settings"`
|
||||
IsPlaceholder bool `json:"is_placeholder,omitempty"`
|
||||
}
|
||||
|
||||
// CustomerSupportDevice represents device information for customer support purposes.
|
||||
@@ -649,8 +650,9 @@ type AttachedProduct struct {
|
||||
|
||||
// ProviderSetting represents a single provider setting.
|
||||
type ProviderSetting struct {
|
||||
BoseID string `json:"bose_id" xml:"boseId"`
|
||||
KeyName string `json:"key_name" xml:"keyName"`
|
||||
Value string `json:"value" xml:"value"`
|
||||
ProviderID string `json:"provider_id" xml:"providerId"`
|
||||
BoseID string `json:"bose_id" xml:"boseId"`
|
||||
KeyName string `json:"key_name" xml:"keyName"`
|
||||
Value string `json:"value" xml:"value"`
|
||||
ProviderID string `json:"provider_id" xml:"providerId"`
|
||||
ProviderName string `json:"provider_name,omitempty" xml:"-"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Package constants defines file names, directories, and common values used by the service layer.
|
||||
package constants
|
||||
|
||||
import "strconv"
|
||||
|
||||
// SourceProvider represents a media source provider configuration.
|
||||
type SourceProvider struct {
|
||||
ID int
|
||||
@@ -97,6 +99,22 @@ func GetSourceLabel(sourceType string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// GetProviderName returns the human-readable name for a provider ID.
|
||||
func GetProviderName(providerID string) string {
|
||||
id, err := strconv.Atoi(providerID)
|
||||
if err != nil {
|
||||
return providerID
|
||||
}
|
||||
|
||||
for _, p := range StaticProviders {
|
||||
if p.ID == id {
|
||||
return p.Name
|
||||
}
|
||||
}
|
||||
|
||||
return providerID
|
||||
}
|
||||
|
||||
// Providers lists known source provider identifiers used by Bose SoundTouch.
|
||||
var Providers = []string{
|
||||
"PANDORA",
|
||||
|
||||
@@ -865,9 +865,10 @@ func (ds *DataStore) GetAccountInfo(accountID string) (*models.ServiceAccountInf
|
||||
return &models.ServiceAccountInfo{AccountID: accountID}, nil
|
||||
}
|
||||
|
||||
// Try account root (canonical location)
|
||||
path := filepath.Join(ds.AccountDir(accountID), "account.json")
|
||||
if !exists(path) {
|
||||
return &models.ServiceAccountInfo{AccountID: accountID}, nil
|
||||
return &models.ServiceAccountInfo{AccountID: accountID, IsPlaceholder: true}, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
@@ -92,6 +92,37 @@ func TestDataStore(t *testing.T) {
|
||||
if ds.AccountDir(account) != expectedAccountDir {
|
||||
t.Errorf("Expected account dir %s, got %s", expectedAccountDir, ds.AccountDir(account))
|
||||
}
|
||||
|
||||
// Test GetAccountInfo with placeholder
|
||||
accInfo, err := ds.GetAccountInfo("non-existent")
|
||||
if err != nil {
|
||||
t.Errorf("GetAccountInfo failed: %v", err)
|
||||
}
|
||||
if !accInfo.IsPlaceholder {
|
||||
t.Errorf("Expected IsPlaceholder to be true for non-existent account")
|
||||
}
|
||||
|
||||
// Test SaveAccountInfo and GetAccountInfo
|
||||
accountID := "acc-123"
|
||||
info2 := &models.ServiceAccountInfo{
|
||||
AccountID: accountID,
|
||||
PreferredLanguage: "en",
|
||||
}
|
||||
err = ds.SaveAccountInfo(accountID, info2)
|
||||
if err != nil {
|
||||
t.Errorf("SaveAccountInfo failed: %v", err)
|
||||
}
|
||||
|
||||
loadedAccInfo, err := ds.GetAccountInfo(accountID)
|
||||
if err != nil {
|
||||
t.Errorf("GetAccountInfo failed: %v", err)
|
||||
}
|
||||
if loadedAccInfo.PreferredLanguage != "en" {
|
||||
t.Errorf("Expected language en, got %s", loadedAccInfo.PreferredLanguage)
|
||||
}
|
||||
if loadedAccInfo.IsPlaceholder {
|
||||
t.Errorf("Expected IsPlaceholder to be false for existing account")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAllDevices_Empty(t *testing.T) {
|
||||
|
||||
@@ -21,6 +21,14 @@ func (s *Server) HandleMgmtAccountDetails(w http.ResponseWriter, r *http.Request
|
||||
accountInfo = &models.ServiceAccountInfo{AccountID: accountID}
|
||||
}
|
||||
|
||||
// Enrich provider settings with names
|
||||
for i := range accountInfo.ProviderSettings {
|
||||
s := &accountInfo.ProviderSettings[i]
|
||||
if s.ProviderName == "" {
|
||||
s.ProviderName = constants.GetProviderName(s.ProviderID)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. List all devices for this account
|
||||
allDevices, err := s.ds.ListAllDevices()
|
||||
if err != nil {
|
||||
@@ -49,6 +57,103 @@ func (s *Server) HandleMgmtAccountDetails(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMgmtUpdateAccountLanguage updates the preferred language for an account.
|
||||
func (s *Server) HandleMgmtUpdateAccountLanguage(w http.ResponseWriter, r *http.Request) {
|
||||
accountID := chi.URLParam(r, "accountId")
|
||||
|
||||
var req struct {
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Language != "en" && req.Language != "de" {
|
||||
http.Error(w, "Language must be 'en' or 'de'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Load current account info
|
||||
accountInfo, err := s.ds.GetAccountInfo(accountID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Update language
|
||||
accountInfo.AccountID = accountID // Ensure ID is correct
|
||||
accountInfo.PreferredLanguage = req.Language
|
||||
accountInfo.IsPlaceholder = false
|
||||
|
||||
// 3. Save account info
|
||||
if err := s.ds.SaveAccountInfo(accountID, accountInfo); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleMgmtUpdateAccountProviderSetting updates a specific provider setting for an account.
|
||||
func (s *Server) HandleMgmtUpdateAccountProviderSetting(w http.ResponseWriter, r *http.Request) {
|
||||
accountID := chi.URLParam(r, "accountId")
|
||||
|
||||
var req struct {
|
||||
ProviderID string `json:"provider_id"`
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.ProviderID == "" || req.Key == "" {
|
||||
http.Error(w, "provider_id and key are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Load current account info
|
||||
accountInfo, err := s.ds.GetAccountInfo(accountID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Update the specific setting
|
||||
found := false
|
||||
|
||||
for i, setting := range accountInfo.ProviderSettings {
|
||||
if setting.ProviderID == req.ProviderID && setting.KeyName == req.Key {
|
||||
accountInfo.ProviderSettings[i].Value = req.Value
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// If not found, we can choose to add it or return error.
|
||||
// For now, let's return an error as we expect to edit existing ones.
|
||||
http.Error(w, "Provider setting not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
accountInfo.AccountID = accountID // Ensure ID is correct
|
||||
accountInfo.IsPlaceholder = false
|
||||
|
||||
// 3. Save account info
|
||||
if err := s.ds.SaveAccountInfo(accountID, accountInfo); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type deviceDetail struct {
|
||||
models.AccountDevice
|
||||
Presets []models.FullResponsePreset `json:"presets,omitempty"`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -147,6 +148,138 @@ func TestHandleMgmtAccountDetails_Recents(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleMgmtUpdateAccountLanguage(t *testing.T) {
|
||||
tempBaseDir := "mgmt_test_data_lang"
|
||||
err := os.MkdirAll(tempBaseDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempBaseDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempBaseDir)
|
||||
err = ds.Initialize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
accountID := "1234567"
|
||||
server := &Server{ds: ds}
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Post("/mgmt/accounts/{accountId}/language", server.HandleMgmtUpdateAccountLanguage)
|
||||
|
||||
t.Run("Valid Language de", func(t *testing.T) {
|
||||
body, _ := json.Marshal(map[string]string{"language": "de"})
|
||||
req := httptest.NewRequest("POST", "/mgmt/accounts/1234567/language", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
accInfo, _ := ds.GetAccountInfo(accountID)
|
||||
if accInfo.PreferredLanguage != "de" {
|
||||
t.Errorf("Expected language 'de', got '%s'", accInfo.PreferredLanguage)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Invalid Language fr", func(t *testing.T) {
|
||||
body, _ := json.Marshal(map[string]string{"language": "fr"})
|
||||
req := httptest.NewRequest("POST", "/mgmt/accounts/1234567/language", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleMgmtUpdateAccountProviderSetting(t *testing.T) {
|
||||
tempBaseDir := "mgmt_test_data_provider"
|
||||
err := os.MkdirAll(tempBaseDir, 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempBaseDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempBaseDir)
|
||||
err = ds.Initialize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
accountID := "1234567"
|
||||
server := &Server{ds: ds}
|
||||
|
||||
// Setup initial account info
|
||||
initialInfo := &models.ServiceAccountInfo{
|
||||
AccountID: accountID,
|
||||
ProviderSettings: []models.ProviderSetting{
|
||||
{
|
||||
ProviderID: "15",
|
||||
KeyName: "STREAMING_QUALITY",
|
||||
Value: "2",
|
||||
},
|
||||
},
|
||||
}
|
||||
ds.SaveAccountInfo(accountID, initialInfo)
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Post("/mgmt/accounts/{accountId}/provider-settings", server.HandleMgmtUpdateAccountProviderSetting)
|
||||
|
||||
t.Run("Valid Update", func(t *testing.T) {
|
||||
payload := map[string]string{
|
||||
"provider_id": "15",
|
||||
"key": "STREAMING_QUALITY",
|
||||
"value": "3",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest("POST", "/mgmt/accounts/1234567/provider-settings", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
accInfo, _ := ds.GetAccountInfo(accountID)
|
||||
found := false
|
||||
for _, s := range accInfo.ProviderSettings {
|
||||
if s.ProviderID == "15" && s.KeyName == "STREAMING_QUALITY" {
|
||||
if s.Value != "3" {
|
||||
t.Errorf("Expected value '3', got '%s'", s.Value)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Provider setting not found after update")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Setting Not Found", func(t *testing.T) {
|
||||
payload := map[string]string{
|
||||
"provider_id": "99",
|
||||
"key": "NON_EXISTENT",
|
||||
"value": "val",
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
req := httptest.NewRequest("POST", "/mgmt/accounts/1234567/provider-settings", bytes.NewBuffer(body))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("Expected status 404, got %d", w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleMgmtAccountDetails_Sources(t *testing.T) {
|
||||
tempBaseDir := "sources_test_data"
|
||||
err := os.MkdirAll(tempBaseDir, 0755)
|
||||
|
||||
@@ -506,13 +506,149 @@ async function fetchAccountDetails(accountId) {
|
||||
|
||||
// Render Metadata
|
||||
if (metadataEl) {
|
||||
const warningNotice = data.account.is_placeholder ?
|
||||
`<div style="background: #fff3cd; color: #856404; padding: 10px; border: 1px solid #ffeeba; border-radius: 4px; margin-bottom: 10px; font-size: 0.85em;">
|
||||
<strong>Notice:</strong> Account data (account.json) was not found in the expected location for this account ID.
|
||||
</div>` : "";
|
||||
|
||||
metadataEl.innerHTML = `
|
||||
${warningNotice}
|
||||
<table style="width: 100%; font-size: 0.9em;">
|
||||
<tr><td style="padding: 4px"><strong>Account ID:</strong></td><td style="padding: 4px">${data.account.account_id}</td></tr>
|
||||
<tr><td style="padding: 4px"><strong>Language:</strong></td><td style="padding: 4px">${data.account.preferred_language || "Not set"}</td></tr>
|
||||
<tr><td style="padding: 4px"><strong>Provider Settings:</strong></td><td style="padding: 4px">${data.account.provider_settings ? "Configured" : "None"}</td></tr>
|
||||
<tr><td style="padding: 4px"><strong>Language:</strong></td><td style="padding: 4px">
|
||||
<select id="account-language-select" style="font-size: 0.9em; padding: 2px;">
|
||||
<option value="en" ${data.account.preferred_language === "en" || !data.account.preferred_language ? "selected" : ""}>en</option>
|
||||
<option value="de" ${data.account.preferred_language === "de" ? "selected" : ""}>de</option>
|
||||
</select>
|
||||
<span id="language-update-status" style="margin-left: 8px; font-size: 0.8em; display: none;">Saving...</span>
|
||||
</td></tr>
|
||||
<tr><td style="padding: 4px"><strong>Provider Settings:</strong></td><td style="padding: 4px">
|
||||
${data.account.provider_settings && data.account.provider_settings.length > 0 ?
|
||||
(() => {
|
||||
const grouped = data.account.provider_settings.reduce((acc, s) => {
|
||||
const pName = s.provider_name || s.provider_id;
|
||||
if (!acc[pName]) acc[pName] = [];
|
||||
acc[pName].push(s);
|
||||
return acc;
|
||||
}, {});
|
||||
return Object.entries(grouped).map(([pName, settings]) => `
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong>${pName}</strong>
|
||||
<ul style="margin: 2px 0 0 0; padding-left: 20px; list-style-type: disc;">
|
||||
${settings.map(s => {
|
||||
if ((s.provider_name === "SPOTIFY" || s.provider_id === "15") && s.key_name === "STREAMING_QUALITY") {
|
||||
return `
|
||||
<li style="margin-bottom: 4px;">
|
||||
Music Streaming Quality:
|
||||
<select class="provider-setting-select"
|
||||
data-account-id="${data.account.account_id}"
|
||||
data-provider-id="${s.provider_id}"
|
||||
data-key="${s.key_name}"
|
||||
style="font-size: 0.9em; padding: 2px; margin-left: 4px;">
|
||||
<option value="1" ${s.value === "1" ? "selected" : ""}>Fastest Streaming - up to 128 kbit/s</option>
|
||||
<option value="2" ${s.value === "2" ? "selected" : ""}>Balanced Quality and Speed - up to 192 kbit/s</option>
|
||||
<option value="3" ${s.value === "3" ? "selected" : ""}>Best Quality - up to 320 kbit/s</option>
|
||||
</select>
|
||||
<span class="setting-update-status" style="margin-left: 8px; font-size: 0.8em; display: none;">Saving...</span>
|
||||
</li>
|
||||
`;
|
||||
}
|
||||
return `<li>${s.key_name}: ${s.value}</li>`;
|
||||
}).join("")}
|
||||
</ul>
|
||||
</div>
|
||||
`).join("");
|
||||
})() : "None"}
|
||||
</td></tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
const languageSelect = document.getElementById("account-language-select");
|
||||
if (languageSelect) {
|
||||
languageSelect.addEventListener("change", async (e) => {
|
||||
const statusEl = document.getElementById("language-update-status");
|
||||
const newLang = e.target.value;
|
||||
if (statusEl) {
|
||||
statusEl.innerText = "Saving...";
|
||||
statusEl.style.display = "inline";
|
||||
statusEl.style.color = "#666";
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/mgmt/accounts/${data.account.account_id}/language`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ language: newLang }),
|
||||
});
|
||||
if (response.ok) {
|
||||
if (statusEl) {
|
||||
statusEl.innerText = "Saved!";
|
||||
statusEl.style.color = "#28a745";
|
||||
setTimeout(() => {
|
||||
statusEl.style.display = "none";
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update language", error);
|
||||
if (statusEl) {
|
||||
statusEl.innerText = "Error!";
|
||||
statusEl.style.color = "#dc3545";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const providerSettingSelects = document.querySelectorAll(".provider-setting-select");
|
||||
providerSettingSelects.forEach(select => {
|
||||
select.addEventListener("change", async (e) => {
|
||||
const statusEl = e.target.parentElement.querySelector(".setting-update-status");
|
||||
const accID = e.target.dataset.accountId;
|
||||
const provID = e.target.dataset.providerId;
|
||||
const key = e.target.dataset.key;
|
||||
const newValue = e.target.value;
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.innerText = "Saving...";
|
||||
statusEl.style.display = "inline";
|
||||
statusEl.style.color = "#666";
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/mgmt/accounts/${accID}/provider-settings`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider_id: provID,
|
||||
key: key,
|
||||
value: newValue
|
||||
}),
|
||||
});
|
||||
if (response.ok) {
|
||||
if (statusEl) {
|
||||
statusEl.innerText = "Saved!";
|
||||
statusEl.style.color = "#28a745";
|
||||
setTimeout(() => {
|
||||
statusEl.style.display = "none";
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to update provider setting", error);
|
||||
if (statusEl) {
|
||||
statusEl.innerText = "Error!";
|
||||
statusEl.style.color = "#dc3545";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Render Devices
|
||||
|
||||
@@ -518,6 +518,13 @@ func AccountFullToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
for i := range resp.ProviderSettings {
|
||||
ps := &resp.ProviderSettings[i]
|
||||
if ps.ProviderName == "" {
|
||||
ps.ProviderName = constants.GetProviderName(ps.ProviderID)
|
||||
}
|
||||
}
|
||||
|
||||
var lastDeviceID string
|
||||
|
||||
for _, entry := range entries {
|
||||
|
||||
Reference in New Issue
Block a user