Skip to content

Commit

Permalink
feat: Music services support (partial)
Browse files Browse the repository at this point in the history
* Initial music service support?
* Finish login support
* Music client login in cli
* Adding Playlist to queue and start stream

Merged #2
  • Loading branch information
svrooij committed Dec 8, 2023
1 parent 75f9660 commit b5ed306
Show file tree
Hide file tree
Showing 57 changed files with 2,435 additions and 91 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -350,3 +350,6 @@ MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

**/launchSettings.json
**/*.runsettings
1 change: 0 additions & 1 deletion src/Sonos.Base/Internal/SonosWebSocket.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Sonos.Base.Services;
using System.Net.WebSockets;
using System.Text.Json;
using System.Threading.Tasks;

namespace Sonos.Base.Internal
{
Expand Down
31 changes: 28 additions & 3 deletions src/Sonos.Base/Metadata/Didl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

[Serializable]
[XmlRoot("DIDL-Lite", Namespace = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/")]

public class Didl
{
[XmlNamespaceDeclarations]
Expand All @@ -15,7 +14,9 @@ public class Didl
new System.Xml.XmlQualifiedName("r", "urn:schemas-rinconnetworks-com:metadata-1-0/"),
});

public Didl() { }
public Didl()
{ }

public Didl(Item item)
{
Items = new[] { item };
Expand All @@ -25,4 +26,28 @@ public Didl(Item item)
// public object[] Rest {get;set;}
[XmlElement("item", Namespace = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/")]
public Item[] Items { get; set; }
}

public static Didl GetMetadataForBroadcast(string? itemId, string didlDescription = "RINCON_AssociatedZPUDN")
{
return new Didl(new Item
{
Class = "object.item.audioItem.audioBroadcast",
Id = $"10092020_{itemId}",
Restricted = true,
Title = "Some stream",
Desc = new DidlDesc(didlDescription)
});
}

public static Didl GetMetadataForPlaylist(string? itemId, string didlDescription = "RINCON_AssociatedZPUDN")
{
return new Didl(new Item
{
Class = "object.container.playlistContainer",
Id = $"1006206c{itemId}".Replace(":", "%3a"),
ParentId = "10fe2664playlists", // From Sonos app
Restricted = true,
Desc = new DidlDesc(didlDescription)
});
}
}
9 changes: 7 additions & 2 deletions src/Sonos.Base/Metadata/DidlDesc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ namespace Sonos.Base.Metadata;
[XmlType(AnonymousType = true, Namespace = "urn:schemas-rinconnetworks-com:metadata-1-0/")]
public class DidlDesc
{
public DidlDesc() { }
public const string Default = "RINCON_AssociatedZPUDN";
public const string SpotifyEurope = "SA_RINCON2311_X_#Svc2311-0-Token";

public DidlDesc()
{ }

public DidlDesc(string value)
{
Value = value;
Expand All @@ -15,5 +20,5 @@ public DidlDesc(string value)
public string Id { get; set; } = "cdudn";

[XmlText]
public string Value { get; set; } = "RINCON_AssociatedZPUDN";
public string Value { get; set; } = Default;
}
16 changes: 10 additions & 6 deletions src/Sonos.Base/Metadata/DidlSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Sonos.Base.Metadata;
public static class DidlSerializer
{
internal const string NotImplemented = "NOT_IMPLEMENTED";

public static Didl? DeserializeMetadata(string? xml)
{
if (xml is null || string.IsNullOrWhiteSpace(xml) || xml == NotImplemented)
Expand All @@ -14,7 +15,7 @@ public static class DidlSerializer
}
var serializer = new XmlSerializer(typeof(Didl));
//serializer.UnknownElement += Serializer_UnknownElement;
// Hack for not parsing elements with namespace prefix defined but not used.
// Hack for not parsing elements with namespace prefix defined but not used.
xml = xml.Replace("<desc ", "<r:desc ").Replace("</desc>", "</r:desc>");
using var textReader = new StringReader(xml);
var result = (Didl?)serializer.Deserialize(textReader);
Expand All @@ -29,7 +30,6 @@ private static void Serializer_UnknownElement(object? sender, XmlElementEventArg
{
if (sender == null)
{

}
}

Expand All @@ -40,21 +40,25 @@ public static string SerializeMetadata(Didl? metadata)
return string.Empty;
}
var overrides = DidlWritingOverrides();
var ns = DidlNamespaces();
var ns = DidlNamespacesForWriting();
var settings = new XmlWriterSettings();
settings.OmitXmlDeclaration = true;
using var stream = new StringWriter();
using var writer = XmlWriter.Create(stream, settings);

var serializer = new XmlSerializer(metadata.GetType(), overrides);
serializer.Serialize(writer, metadata, ns);
return stream.ToString();
var xml = stream.ToString();
return xml.Replace("<r:desc ", "<desc ").Replace("</r:desc", "</desc").Replace("id=\"cdudn\"", "id=\"cdudn\" nameSpace=\"urn:schemas-rinconnetworks-com:metadata-1-0/\"");
}

internal static XmlSerializerNamespaces DidlNamespaces()
internal static XmlSerializerNamespaces DidlNamespacesForWriting()
{
var ns = new XmlSerializerNamespaces();
ns.Add("", ""); // Remove unwanted xsd namespaces.
ns.Add("", "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"); // Remove unwanted xsd namespaces.
ns.Add("dc", "http://purl.org/dc/elements/1.1/");
ns.Add("upnp", "urn:schemas-upnp-org:metadata-1-0/upnp/");
//ns.Add("r", "urn:schemas-rinconnetworks-com:metadata-1-0/");
return ns;
}

Expand Down
2 changes: 2 additions & 0 deletions src/Sonos.Base/Metadata/Item.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ public class Item

[XmlElement("desc", Namespace = "urn:schemas-rinconnetworks-com:metadata-1-0/", Form = System.Xml.Schema.XmlSchemaForm.Qualified)]
public DidlDesc? Desc { get; set; }

public bool ShouldSerializeRestricted() => Restricted == true;
}
10 changes: 10 additions & 0 deletions src/Sonos.Base/Music/AuthenticationType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Sonos.Base.Music
{
public enum AuthenticationType
{
Anonymous,
AppLink,
DeviceLink,
UserId,
}
}
11 changes: 11 additions & 0 deletions src/Sonos.Base/Music/IMusicClientCredentialStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Sonos.Base.Music
{
public interface IMusicClientCredentialStore
{
Task<MusicClientAccount?> GetAccountAsync(int serviceId, CancellationToken cancellationToken = default);

Task<bool> SaveAccountAsync(int serviceId, string key, string token, CancellationToken cancellationToken = default);
}

public record MusicClientAccount(string Key, string Token);
}
65 changes: 65 additions & 0 deletions src/Sonos.Base/Music/KvMusicServiceAccountStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Sonos.Base.Services;
using System.Text.Json;

namespace Sonos.Base.Music
{
public class KvMusicServiceAccountStore : IMusicClientCredentialStore
{
private readonly SystemPropertiesService _propertiesService;
private readonly ILogger<KvMusicServiceAccountStore> _logger;
private readonly Dictionary<int, MusicClientAccount> store = new Dictionary<int, MusicClientAccount>();

public KvMusicServiceAccountStore(SystemPropertiesService propertiesService, ILogger<KvMusicServiceAccountStore>? logger)
{
_propertiesService = propertiesService;
_logger = logger ?? NullLogger<KvMusicServiceAccountStore>.Instance;
}

public async Task<MusicClientAccount?> GetAccountAsync(int serviceId, CancellationToken cancellationToken = default)
{
_logger.LogDebug("Loading account for service {serviceId}", serviceId);
if (!store.ContainsKey(serviceId))
{
try
{
var key = GetKeyForService(serviceId);
var data = await _propertiesService.GetStringAsync(key, cancellationToken);
var account = JsonSerializer.Deserialize<MusicClientAccount>(data);
if (account == null)
{
return null;
}
store[serviceId] = account;
return account;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error getting account for {serviceId}", serviceId);
return null;
}
}
return store[serviceId];
}

public async Task<bool> SaveAccountAsync(int serviceId, string key, string token, CancellationToken cancellationToken = default)
{
var account = new MusicClientAccount(key, token);
store[serviceId] = account;

try
{
var result = await _propertiesService.SetStringAsync(GetKeyForService(serviceId), JsonSerializer.Serialize(account), cancellationToken);
return result;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error saving account for {serviceId}", serviceId);
return false;
}
}

private string GetKeyForService(int serviceId) => $"SonosNet-Music-{serviceId}";
}
}
37 changes: 37 additions & 0 deletions src/Sonos.Base/Music/MemoryMusicServiceAccountStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace Sonos.Base.Music
{
internal class MemoryMusicServiceAccountStore : IMusicClientCredentialStore
{
private readonly ILogger<MemoryMusicServiceAccountStore> logger;

public MemoryMusicServiceAccountStore(ILogger<MemoryMusicServiceAccountStore>? logger = null)
{
this.logger = logger ?? NullLogger<MemoryMusicServiceAccountStore>.Instance;
}

private readonly Dictionary<int, MusicClientAccount> store = new Dictionary<int, MusicClientAccount>();

public async Task<MusicClientAccount?> GetAccountAsync(int serviceId, CancellationToken cancellationToken = default)
{
logger.LogDebug("Loading account for service {serviceId}", serviceId);
return store.ContainsKey(serviceId) ? store[serviceId] : null;
}

public async Task<bool> SaveAccountAsync(int serviceId, string key, string token, CancellationToken cancellationToken = default)
{
logger.LogDebug("Saving account for service {serviceId}", serviceId);
if (store.ContainsKey(serviceId))
{
store[serviceId] = new MusicClientAccount(key, token);
}
else
{
store.Add(serviceId, new MusicClientAccount(key, token));
}
return true;
}
}
}
76 changes: 76 additions & 0 deletions src/Sonos.Base/Music/Models/GetAppLink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;

namespace Sonos.Base.Music.Models
{
[Serializable()]
[XmlRoot(Namespace = "http://www.sonos.com/Services/1.1")]
public class GetAppLinkRequest : MusicClientBaseRequest
{
/// <summary>
/// Load from DevicePropertiesService.GetHouseholdID()
/// </summary>
[XmlElement(ElementName = "householdId", Namespace = "http://www.sonos.com/Services/1.1")]
public string HouseholdId { get; set; }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[XmlType(AnonymousType = true, Namespace = "http://www.sonos.com/Services/1.1")]
[XmlRoot(Namespace = "http://www.sonos.com/Services/1.1", IsNullable = false)]
public partial class GetAppLinkResponse : ISmapiResponse<GetAppLinkResult>
{
[XmlElement("getAppLinkResult")]
public GetAppLinkResult Result { get; set; }
}

/// <remarks/>
[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[XmlType(AnonymousType = true, Namespace = "http://www.sonos.com/Services/1.1")]
public partial class GetAppLinkResult
{
[XmlElement("authorizeAccount")]
public GetAppLinkAuthorizeAccount AuthorizeAccount { get; set; }

[XmlElement("createAccount")]
public GetAppLinkAccount CreateAccount { get; set; }
}

[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[XmlType(AnonymousType = true, Namespace = "http://www.sonos.com/Services/1.1")]
public partial class GetAppLinkAccount
{
[XmlElement("appUrlStringId")]
public string AppUrlStringId { get; set; }
}

[Serializable()]
[System.ComponentModel.DesignerCategory("code")]
[XmlType(AnonymousType = true, Namespace = "http://www.sonos.com/Services/1.1")]
public partial class GetAppLinkAuthorizeAccount : GetAppLinkAccount
{
[XmlElement("deviceLink")]
public GetAppLinkDeviceLink DeviceLink { get; set; }
}


public partial class GetAppLinkDeviceLink
{
[XmlElement("regUrl")]

public string RegistrationUrl { get; set; }
[XmlElement("linkCode")]

public string LinkCode { get; set; }
[XmlElement("showLinkCode")]

public bool ShowLinkCode { get; set; }
}
}
Loading

0 comments on commit b5ed306

Please sign in to comment.