Skip to content

Commit

Permalink
v.2.6.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Difegue committed Sep 20, 2023
2 parents 03b7fda + 9e6a668 commit 1567683
Show file tree
Hide file tree
Showing 82 changed files with 2,312 additions and 2,205 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Stylophone
[**Music Player Daemon**](https://www.musicpd.org/) Client for UWP and iOS/iPadOS.
Based on [MpcNET](https://github.com/Difegue/MpcNET), my own fork of the original .NET Client Library for MPD. (now on NuGet!)

<a href='//www.microsoft.com/store/apps/9NCB693428T8?cid=storebadge&ocid=badge'><img src='https://developer.microsoft.com/en-us/store/badges/images/English_get-it-from-MS.png' alt='English badge' width="142" height="52"/></a>
[<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>](https://www.microsoft.com/store/apps/9NCB693428T8?cid=storebadge&ocid=badge) [<img src="https://developer.apple.com/assets/elements/badges/download-on-the-app-store.svg" width="216"/>](https://apps.apple.com/us/app/stylophone/id1644672889?itsct=apps_box_link&itscg=30200)

[Buy a sticker if you want!](https://ko-fi.com/s/9fcf421b6e)

Expand Down
6 changes: 6 additions & 0 deletions Sources/Stylophone.Common/Helpers/ColorThief.Skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ public QuantizedColor GetColor(SKBitmap sourceImage, int quality = DefaultQualit
{
var palette = GetPalette(sourceImage, 3, quality, ignoreWhite);

// Handle case where GetPalette returns an empty list (because GetColorMap failed?)
if (palette.Count == 0)
{
return new QuantizedColor(SKColors.Black, 1);
}

var avgR = Convert.ToByte(palette.Average(a => a.Color.Red));
var avgG = Convert.ToByte(palette.Average(a => a.Color.Green));
var avgB = Convert.ToByte(palette.Average(a => a.Color.Blue));
Expand Down
21 changes: 21 additions & 0 deletions Sources/Stylophone.Common/Helpers/Miscellaneous.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using SkiaSharp;
using System;
using System.IO;
using System.Net;

namespace Stylophone.Common.Helpers
{
Expand Down Expand Up @@ -83,5 +84,25 @@ public static string EscapeFilename(string fileName)
}
return fileName.Replace(".", "002E");
}

public static IPEndPoint GetIPEndPointFromHostName(string hostName, int port, bool throwIfMoreThanOneIP)
{
var addresses = System.Net.Dns.GetHostAddresses(hostName);
if (addresses.Length == 0)
{
throw new ArgumentException(
"Unable to retrieve address from specified host name.",
"hostName"
);
}
else if (throwIfMoreThanOneIP && addresses.Length > 1)
{
throw new ArgumentException(
"There is more that one IP address to the specified host.",
"hostName"
);
}
return new IPEndPoint(addresses[0], port); // Port gets validated here.
}
}
}
170 changes: 170 additions & 0 deletions Sources/Stylophone.Common/Helpers/RangedObservableCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;

namespace System.Collections.ObjectModel
{
/// <summary>
/// Implementation of a dynamic data collection based on generic Collection&lt;T&gt;,
/// implementing INotifyCollectionChanged to notify listeners
/// when items get added, removed or the whole list is refreshed.
/// </summary>
public class RangedObservableCollection<T> : ObservableCollection<T>
{
private const string CountName = "Count";
private const string IndexerName = "Item[]";

/// <summary>
/// Initializes a new instance of RangedObservableCollection that is empty and has default initial capacity.
/// </summary>
public RangedObservableCollection()
: base()
{
}

/// <summary>
/// Initializes a new instance of the RangedObservableCollection class that contains
/// elements copied from the specified collection and has sufficient capacity
/// to accommodate the number of elements copied.
/// </summary>
/// <param name="collection">The collection whose elements are copied to the new list.</param>
/// <remarks>
/// The elements are copied onto the RangedObservableCollection in the
/// same order they are read by the enumerator of the collection.
/// </remarks>
/// <exception cref="ArgumentNullException"> collection is a null reference </exception>
public RangedObservableCollection(IEnumerable<T> collection)
: base(collection)
{
}

/// <summary>
/// Adds the elements of the specified collection to the end of the RangedObservableCollection.
/// </summary>
/// <param name="collection">The collection whose elements are added to the list.</param>
/// <remarks>
/// The elements are copied onto the RangedObservableCollection in the
/// same order they are read by the enumerator of the collection.
/// </remarks>
/// <exception cref="ArgumentNullException"> collection is a null reference </exception>
public void AddRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));

CheckReentrancy();

var startIndex = Count;
var changedItems = new List<T>(collection);

foreach (var i in changedItems)
Items.Add(i);

OnCountPropertyChanged();
OnIndexerPropertyChanged();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, changedItems, startIndex));
}

/// <summary>
/// Clears the current RangedObservableCollection and replaces the elements with the elements of specified collection.
/// </summary>
/// <param name="collection">The collection whose elements are added to the list.</param>
/// <remarks>
/// The elements are copied onto the RangedObservableCollection in the
/// same order they are read by the enumerator of the collection.
/// </remarks>
/// <exception cref="ArgumentNullException"> collection is a null reference </exception>
public void ReplaceRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));

CheckReentrancy();

Items.Clear();
foreach (var i in collection)
Items.Add(i);

OnCountPropertyChanged();
OnIndexerPropertyChanged();
OnCollectionReset();
}

/// <summary>
/// Removes the first occurence of each item in the specified collection.
/// </summary>
/// <param name="collection">The collection whose elements are removed from list.</param>
/// <remarks>
/// The elements are copied onto the RangedObservableCollection in the
/// same order they are read by the enumerator of the collection.
/// </remarks>
/// <exception cref="ArgumentNullException"> collection is a null reference </exception>
public void RemoveRange(IEnumerable<T> collection)
{
if (collection == null)
throw new ArgumentNullException(nameof(collection));

CheckReentrancy();

// HACK ATTACK: normally, since this method can remove items from multiple different spaces in the collection, this'd be incorrect...
// but since we only use it for ranged deletions of queue items, which are always sequential, it should be fine(tm)
var index = Items.IndexOf(collection.FirstOrDefault());

var changedItems = new List<T>(collection);
for (int i = 0; i < changedItems.Count; i++)
{
if (!Items.Remove(changedItems[i]))
{
changedItems.RemoveAt(i);
i--;
}
}

if (changedItems.Count > 0)
{
OnCountPropertyChanged();
OnIndexerPropertyChanged();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, changedItems, index));
}
}

/// <summary>
/// Clears the current RangedObservableCollection and replaces the elements with the specified element.
/// </summary>
/// <param name="item">The element which is added to the list.</param>
public void Replace(T item)
{
CheckReentrancy();

Items.Clear();
Items.Add(item);

OnCountPropertyChanged();
OnIndexerPropertyChanged();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

private void OnCountPropertyChanged()
{
OnPropertyChanged(EventArgsCache.CountPropertyChanged);
}

private void OnIndexerPropertyChanged()
{
OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
}

private void OnCollectionReset()
{
OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
}

private static class EventArgsCache
{
internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs(CountName);
internal static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs(IndexerName);
internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
}
}
}
6 changes: 5 additions & 1 deletion Sources/Stylophone.Common/Services/AlbumArtService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public void Initialize()
{
_queueCanceller?.Cancel();
_queueCanceller = new CancellationTokenSource();

var token = _queueCanceller.Token;

_albumArtQueue = new Stack<AlbumViewModel>();
Expand Down Expand Up @@ -81,6 +80,11 @@ public void Initialize()
}).ConfigureAwait(false);
}

public void Stop()
{
_queueCanceller?.Cancel();
}

/// <summary>
/// Check if this file's album art is already stored in the internal Album Art cache.
/// </summary>
Expand Down
73 changes: 47 additions & 26 deletions Sources/Stylophone.Common/Services/MPDConnectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
using Stylophone.Common.Interfaces;
using MpcNET.Commands.Reflection;
using Stylophone.Localization.Strings;
using Stylophone.Common.Helpers;
using CommunityToolkit.Mvvm.DependencyInjection;
using Stylophone.Common.ViewModels;
using Microsoft.AppCenter.Analytics;
using Microsoft.AppCenter;
using System.Drawing;
using Stylophone.Common.ViewModels;

namespace Stylophone.Common.Services
{
Expand Down Expand Up @@ -77,13 +76,7 @@ public async Task InitializeAsync(bool withRetry = false)
IsConnecting = true;
CurrentStatus = BOGUS_STATUS; // Reset status

if (IsConnected)
{
IsConnected = false;
ConnectionChanged?.Invoke(this, new EventArgs());
}

ClearResources();
Disconnect();

var cancelToken = _cancelConnect.Token;

Expand All @@ -106,23 +99,31 @@ public async Task InitializeAsync(bool withRetry = false)
_connectionRetryAttempter.Start();
}
}

IsConnecting = false;
}

private void ClearResources()
public void Disconnect()
{
_idleConnection?.SendAsync(new NoIdleCommand());
_idleConnection?.DisconnectAsync();
_statusConnection?.DisconnectAsync();

if (IsConnected)
{
System.Diagnostics.Debug.WriteLine($"Terminating MPD connections");
IsConnected = false;
ConnectionChanged?.Invoke(this, new EventArgs());
}

// Stop the idle connection first
_cancelIdle?.Cancel();

_connectionRetryAttempter?.Stop();
_connectionRetryAttempter?.Dispose();

// Stop the status timer before killing the matching connection
_statusUpdater?.Stop();
_statusUpdater?.Dispose();
_statusConnection?.DisconnectAsync();

_cancelIdle?.Cancel();
_cancelIdle = new CancellationTokenSource();

_cancelConnect?.Cancel();
Expand All @@ -137,10 +138,16 @@ private void ClearResources()
private async Task TryConnecting(CancellationToken token)
{
if (token.IsCancellationRequested) return;
if (!IPAddress.TryParse(_host, out var ipAddress))
throw new Exception("Invalid IP address");

_mpdEndpoint = new IPEndPoint(ipAddress, _port);
if (!IPAddress.TryParse(_host, out var ipAddress))
{
// Maybe it's a hostname? Try getting an IP from it nonetheless
_mpdEndpoint = Miscellaneous.GetIPEndPointFromHostName(_host, _port, false);
}
else
{
_mpdEndpoint = new IPEndPoint(ipAddress, _port);
}

_statusConnection = await GetConnectionInternalAsync(token);

Expand Down Expand Up @@ -223,8 +230,12 @@ public async Task<T> SafelySendCommandAsync<T>(IMpcCommand<T> command, bool show
{
var dict = new Dictionary<string, string>();
dict.Add("command", command.Serialize());
dict.Add("exception", e.ToString());
Analytics.TrackEvent("MPDError", dict);
dict.Add("exception", e.InnerException?.ToString());
dict.Add("source", e.Source);
dict.Add("message", e.Message);
dict.Add("stacktrace", e.StackTrace);

Analytics.TrackEvent("MPDError", dict);
}
#endif
}
Expand Down Expand Up @@ -270,15 +281,25 @@ private void InitializeStatusUpdater(CancellationToken token = default)
try
{
// Run the idleConnection in a wrapper task since MpcNET isn't fully async and will block here
var idleChangesTask = Task.Run(async () => await _idleConnection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update")));
// Wait for the idle command to finish or for the token to be cancelled
await Task.WhenAny(idleChangesTask, Task.Delay(-1, token));
if (token.IsCancellationRequested || _idleConnection == null || !_idleConnection.IsConnected)
{
//_idleConnection?.SendAsync(new NoIdleCommand());
_idleConnection?.DisconnectAsync();
break;
}
var message = idleChangesTask.Result;
var idleChanges = await _idleConnection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update"));
if (idleChanges.IsResponseValid)
await HandleIdleResponseAsync(idleChanges.Response.Content);
if (message.IsResponseValid)
await HandleIdleResponseAsync(message.Response.Content);
else
throw new Exception(idleChanges.Response?.Content);
throw new Exception(message.Response?.Content);
}
catch (Exception e)
{
Expand Down
Loading

0 comments on commit 1567683

Please sign in to comment.