Skip to content

Commit

Permalink
Merge pull request #1 from svrooij/ci/release-events
Browse files Browse the repository at this point in the history
chore: Event release cleanup
  • Loading branch information
svrooij committed May 26, 2023
2 parents bd21ab2 + 8351ea5 commit 3ed823d
Show file tree
Hide file tree
Showing 16 changed files with 494 additions and 45 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,10 @@ jobs:
run: dotnet nuget push ./src/Sonos.Base/bin/Release/Sonos.Base.*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json
env:
NUGET_AUTH_TOKEN: ${{ secrets.NUGET_TOKEN }}

- name: Pack Sonos.Base.Events.Http
run: dotnet pack --configuration Release --no-build --no-restore ./src/Sonos.Base.Events.Http/Sonos.Base.Events.Http.csproj
- name: Publish Sonos.Base.Events.Http to nuget.org
run: dotnet nuget push ./src/Sonos.Base.Events.Http/bin/Release/Sonos.Base.*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json
env:
NUGET_AUTH_TOKEN: ${{ secrets.NUGET_TOKEN }}
5 changes: 5 additions & 0 deletions Sonos.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sonos.Base.Events.Http.Test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sonos.Base.Events.Http", "src\Sonos.Base.Events.Http\Sonos.Base.Events.Http.csproj", "{E889E215-7C1F-40B2-8CCA-D6ACF3CDDB18}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "misc", "misc", "{747CF25D-9739-47DF-A54D-1F745E7ED40C}"
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
7 changes: 6 additions & 1 deletion src/Sonos.Base.Events.Http/Parsing/AvTransportEventRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

namespace Sonos.Base.Events.Http.Parsing;

/// <summary>
/// AVTransportEventRoot
/// </summary>
[Serializable]
[XmlType(AnonymousType = true, Namespace = "urn:schemas-upnp-org:metadata-1-0/AVT/")]
[XmlRoot("Event", Namespace = "urn:schemas-upnp-org:metadata-1-0/AVT/", IsNullable = false)]

public partial class AVTransportEventRoot : IParsedEvent<Models.AVTransportEvent>
{
[XmlElement("InstanceID")]
Expand All @@ -20,6 +22,9 @@ public partial class AVTransportEventRoot : IParsedEvent<Models.AVTransportEvent
public AVTransportEvent? GetEvent() => AVTransportEvent.FromDictionary(Instance?.GetEventProperties());
}

/// <summary>
/// AVTransportEventInstance
/// </summary>
[XmlType(AnonymousType = true, Namespace = "urn:schemas-upnp-org:metadata-1-0/AVT/")]

public partial class AVTransportEventInstance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Sonos.Base.Events.Http.Parsing;

/// <summary>
/// RenderingControlEventRoot
/// </summary>
[Serializable]
[XmlType(AnonymousType = true, Namespace = "urn:schemas-upnp-org:metadata-1-0/RCS/")]
[XmlRoot("Event", Namespace = "urn:schemas-upnp-org:metadata-1-0/RCS/", IsNullable = false)]
Expand All @@ -18,6 +21,9 @@ public partial class RenderingControlEventRoot : IParsedEvent<Models.RenderingCo
public RenderingControlEvent? GetEvent() => RenderingControlEvent.FromDictionary(Instance?.GetEventProperties());
}

/// <summary>
/// RenderingControlEventInstance
/// </summary>
[XmlType(AnonymousType = true, Namespace = "urn:schemas-upnp-org:metadata-1-0/RCS/")]

public partial class RenderingControlEventInstance
Expand Down
65 changes: 65 additions & 0 deletions src/Sonos.Base.Events.Http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Http based event receiver for Sonos.Base

[![Latest version][badge_nuget]][link_nuget]
[![Github Issues][badge_issues]][link_issues]
[![Star on Github][badge_repo_stars]][link_repo]

Always wanted to control sonos speakers right from your dotnet application? I've created this library for you to do just that.

**Warning** This library is far from complete at the moment, it's just an experiment for now check [features](#features) for more details.

## Control sonos in other languages

[![Sonos net][badge_sonos-csharp]][link_repo]
[![Sonos typescript][badge_sonos-typescript]][link_sonos-typescript]
[![Sonos api documentation][badge_sonos-docs]][link_sonos-docs]
[![Sonos2mqtt][badge_sonos-mqtt]][link_sonos-mqtt]

## Show your support

[![Support me on Github][badge_sponsor]][link_sponsor]
[![Follow on Twitter][badge_twitter]][link_twitter]
[![Check my blog][badge_blog]][link_blog]

## Features

Currently this library is not at version one, these are the planned features that need to be build before this can be called a version one.

- [X] All sonos services generated from service discovery
- [X] Event subscriptions
- [ ] Dynamic metadata generation based on [these docs](https://svrooij.io/sonos-api-docs/metadata.html)
- [ ] SonosManager class that keeps track of speaker groups
- [ ] Device discovery, though it hardly works

## CodeTour available

This project uses [CodeTour](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.codetour) in [Visual Studio Code](https://code.visualstudio.com/) to describe how stuff works. If you want to contribute to this library, I suggest you to take a look at the code tour just to get started.

## Contribute

We welcome all contributions to this project, to get started be sure to checkout the [CodeTour](#codetour-available) which will explain how some files get generated.

If you see a file with the `.gen.cs` suffix it means that it is generated. Manual changes to these files will not be accepted because they will get lost upon next generation.

This library is [licensed](./LICENSE.md) under **GPL v3** and all contributions are considered to be publishable under that same license.

[badge_blog]: https://img.shields.io/badge/blog-svrooij.io-blue?style=for-the-badge
[badge_issues]: https://img.shields.io/github/issues/svrooij/sonos-net?style=for-the-badge
[badge_nuget]: https://img.shields.io/nuget/v/Sonos.Base.Events.Http?style=for-the-badge
[badge_sonos-csharp]: https://img.shields.io/badge/sonos-C%23-blue?style=for-the-badge
[badge_sonos-docs]: https://img.shields.io/badge/sonos-documentation-blue?style=for-the-badge
[badge_sonos-mqtt]: https://img.shields.io/badge/sonos-mqtt-blue?style=for-the-badge
[badge_sonos-typescript]: https://img.shields.io/badge/sonos-typescript-blue?style=for-the-badge
[badge_sponsor]: https://img.shields.io/github/sponsors/svrooij?logo=github&style=for-the-badge
[badge_repo_stars]: https://img.shields.io/github/stars/svrooij/sonos-net?logo=github&style=for-the-badge
[badge_twitter]: https://img.shields.io/twitter/follow/svrooij?logo=twitter&style=for-the-badge

[link_blog]: https://svrooij.io
[link_issues]: https://github.com/svrooij/sonos-api-docs/issues
[link_nuget]: https://www.nuget.org/packages/Sonos.Base/
[link_sonos-docs]: https://svrooij.io/sonos-api-docs
[link_sonos-mqtt]: https://svrooij.io/sonos2mqtt
[link_sonos-typescript]: https://svrooij.io/node-sonos-ts
[link_sponsor]: https://github.com/sponsors/svrooij
[link_repo]: https://github.com/svrooij/sonos-net
[link_twitter]: https://twitter.com/svrooij
39 changes: 25 additions & 14 deletions src/Sonos.Base.Events.Http/Sonos.Base.Events.Http.csproj
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<PackageId>Sonos.Base.Events.Http</PackageId>
<Authors>Stephan van Rooij</Authors>
<PackageProjectUrl>https://github.com/svrooij/sonos-net</PackageProjectUrl>
<RepositoryUrl>https://github.com/svrooij/sonos-net.git</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
<RepositoryType>git</RepositoryType>
<PackageTags>Sonos</PackageTags>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0|AnyCPU'">
<NoWarn>1701;1702;CS8616</NoWarn>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;CS8616;CS1591</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0|AnyCPU'">
<NoWarn>1701;1702;CS8616</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net6.0|AnyCPU'">
<NoWarn>1701;1702;CS8616</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net6.0|AnyCPU'">
<NoWarn>1701;1702;CS8616</NoWarn>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;CS8616;CS1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Sonos.Base\Sonos.Base.csproj" />
</ItemGroup>
<ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' ">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net7.0' ">
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0' ">
Expand All @@ -36,4 +42,9 @@
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
</ItemGroup>

<ItemGroup>
<None Include=".\README.md" Pack="true" PackagePath="\README.md" />
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\LICENSE.md" />
</ItemGroup>

</Project>
90 changes: 72 additions & 18 deletions src/Sonos.Base.Events.Http/SonosEventReceiver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,41 @@
using System.Xml.Serialization;

namespace Sonos.Base.Events.Http;

/// <summary>
/// SonosEventReceiver is an Http based sonos event receiver, which implements a receiver that can subscribe to Sonos UPNP events
/// </summary>
/// <remarks>Add as IHostedService to your project.</remarks>
public partial class SonosEventReceiver : IHostedService, ISonosEventBus
{
private readonly HttpClient httpClient;
private readonly ILogger logger;
private readonly ILogger<SonosEventReceiver> logger;
private readonly SonosEventReceiverOptions options;
private readonly ConcurrentBag<SonosEventSubscription> subscriptions = new ConcurrentBag<SonosEventSubscription>();
private WebApplication WebApplication;

public SonosEventReceiver(HttpClient httpClient, ILogger<SonosEventReceiver>? logger = null, IOptions<SonosEventReceiverOptions>? settings = null)
/// <summary>
/// SonosEventReceiver constructor
/// </summary>
/// <param name="httpClient">Configure your http client</param>
/// <param name="logger">Log events from the actual receiver</param>
/// <param name="loggerProvider">Pass through the main logger provider to the child web host for events</param>
/// <param name="settings">SonosEventReceiverOptions settings</param>
/// <remarks>Probably called from dependency injection</remarks>
public SonosEventReceiver(HttpClient? httpClient = null, ILogger<SonosEventReceiver>? logger = null, ILoggerProvider? loggerProvider = null, IOptions<SonosEventReceiverOptions>? settings = null)
{
this.logger = logger ?? (ILogger)NullLogger.Instance;
this.httpClient = httpClient;
this.logger = logger ?? NullLogger<SonosEventReceiver>.Instance;
this.httpClient = httpClient ?? new HttpClient();
options = settings?.Value ?? new SonosEventReceiverOptions();
WebApplication = CreateWebApplication(options.Port);
WebApplication = CreateWebApplication(options.Port, loggerProvider);
}

/// <summary>
/// Renew a subscription to a sonos service
/// </summary>
/// <param name="uuid">UUID of the player</param>
/// <param name="service">Sonos Service that wants the events</param>
/// <param name="cancellationToken">CancellationToken to cancel the renew subscription request</param>
/// <returns>true when the subscription was renewed successfully</returns>
public async Task<bool> RenewSubscription(string uuid, SonosService service, CancellationToken cancellationToken = default)
{
var subscription = GetSubscription(uuid, service);
Expand All @@ -49,27 +67,38 @@ public async Task<bool> RenewSubscription(string uuid, SonosService service, Can
return response.IsSuccessStatusCode;
}

public Task RunAsync(CancellationToken cancellationToken)
{
return WebApplication.RunAsync(token: cancellationToken);
}

public void RunBlocked()
{
WebApplication.Run();
}

/// <summary>
/// IHostedService Implementation to start the child web application
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public Task StartAsync(CancellationToken cancellationToken)
{
LogStartAsyncCalled();
return WebApplication.StartAsync(cancellationToken);
}

/// <summary>
/// IHostedService implementation to stop the child web application
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task StopAsync(CancellationToken cancellationToken)
{
LogStopAsyncCalled();
await UnsubscribeAll(cancellationToken);
await WebApplication.StopAsync(cancellationToken);
}

/// <summary>
/// Subscribe for events from this Sonos service
/// </summary>
/// <param name="uuid">UUID of the player</param>
/// <param name="service">Sonos Service that wants the events</param>
/// <param name="eventEndpoint">Endpoint of the events</param>
/// <param name="callback">Action that is called when a new event is parsed</param>
/// <param name="cancellationToken">CancellationToken to cancel the create subscription request</param>
/// <returns>true when the subscription was made successfully</returns>
public async Task<bool> Subscribe(string uuid, SonosService service, Uri eventEndpoint, Action<IServiceEvent> callback, CancellationToken cancellationToken = default)
{
int timeoutInSeconds = 1800;
Expand Down Expand Up @@ -104,6 +133,13 @@ public async Task<bool> Subscribe(string uuid, SonosService service, Uri eventEn
return true;
}

/// <summary>
/// Cancel an existing subscription, so the events will stop
/// </summary>
/// <param name="uuid">UUID of the player</param>
/// <param name="service">Sonos Service you subscribed to</param>
/// <param name="cancellationToken">CancellationToken to cancel the cancel subscription request</param>
/// <returns>true when the unsubscription request was successfull</returns>
public async Task<bool> Unsubscribe(string uuid, SonosService service, CancellationToken cancellationToken = default)
{
LogUnsubscribeStart(uuid, service);
Expand All @@ -119,6 +155,11 @@ public async Task<bool> Unsubscribe(string uuid, SonosService service, Cancellat
return result;
}

/// <summary>
/// Cancel all existing subscriptions, so the events will stop
/// </summary>
/// <param name="cancellationToken">CancellationToken to cancel the cancel subscription request</param>
/// <returns>true when then ubsubscription request was successfull</returns>
public async Task<bool> UnsubscribeAll(CancellationToken cancellationToken = default)
{
LogUnsubscribeAllStart();
Expand Down Expand Up @@ -148,7 +189,7 @@ public async Task<bool> UnsubscribeAll(CancellationToken cancellationToken = def
var data = (IParsedEvent<TEvent>?)serializer.Deserialize(eventReader);

TEvent? actualEvent = data?.GetEvent();
logger?.LogDebug("{service} event received {@event}", service, actualEvent);
logger.LogDebug("{service} event received {@event}", service, actualEvent);
EmitParsedEvent(uuid, service, actualEvent);
}

Expand Down Expand Up @@ -278,7 +319,7 @@ internal async Task<IResult> HandleTestAsync(HttpContext ctx)
throw new NotImplementedException();
}

private WebApplication CreateWebApplication(int port)
private WebApplication CreateWebApplication(int port, ILoggerProvider? loggerProvider = null)
{
//var builder = WebApplication.CreateBuilder(new WebApplicationOptions {
// Args = new[]
Expand All @@ -288,6 +329,13 @@ private WebApplication CreateWebApplication(int port)
//});
var builder = WebApplication.CreateBuilder();

builder.Logging.ClearProviders();
if (loggerProvider != null)
{
builder.Logging.AddProvider(loggerProvider);
}


//builder.Logging.AddJsonConsole();
//builder.Logging.AddConsole();
var app = builder.Build();
Expand All @@ -310,6 +358,12 @@ private string GenerateCallback(string uuid, SonosService service)

private SonosEventSubscription? GetSubscription(string uuid, SonosService service) => subscriptions.FirstOrDefault(s => s.Uuid == uuid && s.Service == service);

[LoggerMessage(EventId = 21, Level = LogLevel.Information, Message = "StartAsync Called")]
private partial void LogStartAsyncCalled();

[LoggerMessage(EventId = 22, Level = LogLevel.Information, Message = "StopAsync Called")]
private partial void LogStopAsyncCalled();

[LoggerMessage(EventId = 201, Level = LogLevel.Debug, Message = "Empty event received {Uuid}/{Service}")]
private partial void LogEventEmpty(string uuid, SonosService service);

Expand Down
Loading

0 comments on commit 3ed823d

Please sign in to comment.