8 Commits

15 changed files with 614 additions and 188 deletions

View File

@ -1,27 +1,5 @@
# Changelog
## v1.3.1
*Released: 03/11/2022*
* Update .NET version from 5.0 to 6.0
* Fix: simple patch to avoid a crash concerning GetAlbumCover
* Fix: connection change now working
## v1.3
*Released: 18/04/2022*
* New feature: add support for readpicture, aka embedded cover art
* New feature: add support for MPD password
* Spanish translation
* Trim album release date
* Cover icon when a radio is playing
* Update MpcNET package from 1.3 to 1.4
* Fix: Snapcast not working with hostname
* Fix: some radios crash
* Fix: disable/enable radios and Snapcast with connection
## v1.2
*Released: 07/04/2022*

View File

@ -64,19 +64,17 @@ namespace unison
private MpcConnection _connection;
private MpcConnection _commandConnection;
private IPEndPoint _mpdEndpoint;
private CancellationTokenSource _cancelCommand;
private CancellationTokenSource _cancelConnect;
private CancellationTokenSource _cancelToken;
public MPDHandler()
{
Startup(null, null);
Initialize(null, null);
_stats = new Statistics();
_retryTimer = new DispatcherTimer();
_retryTimer.Interval = TimeSpan.FromSeconds(5);
_retryTimer.Tick += Startup;
_retryTimer.Tick += Initialize;
_elapsedTimer = new System.Timers.Timer(500);
_elapsedTimer.Elapsed += new System.Timers.ElapsedEventHandler(ElapsedTimer);
@ -149,7 +147,7 @@ namespace unison
public async Task<T> SafelySendCommandAsync<T>(IMpcCommand<T> command)
{
if (_commandConnection == null || !IsConnected())
if (_commandConnection == null)
{
Trace.WriteLine("[SafelySendCommandAsync] no command connection");
return default(T);
@ -177,52 +175,21 @@ namespace unison
return default(T);
}
public async void Startup(object sender, EventArgs e)
private void Initialize(object sender, EventArgs e)
{
await Initialize();
}
public async Task Initialize()
{
Trace.WriteLine("Initializing");
Disconnected();
if (!_connected)
await Connect();
Connect();
}
public void Disconnected()
public async void Connect()
{
_connected = false;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
_commandConnection?.DisconnectAsync();
_connection?.DisconnectAsync();
_cancelConnect?.Cancel();
_cancelConnect = new CancellationTokenSource();
_cancelCommand?.Cancel();
_cancelCommand = new CancellationTokenSource();
_connection = null;
_commandConnection = null;
Trace.WriteLine("Disconnected");
}
public async Task Connect()
{
Trace.WriteLine("Connecting");
if (_cancelCommand.IsCancellationRequested || _cancelConnect.IsCancellationRequested)
return;
_cancelToken = new CancellationTokenSource();
CancellationToken token = _cancelToken.Token;
try
{
_connection = await ConnectInternal(_cancelConnect.Token);
_commandConnection = await ConnectInternal(_cancelCommand.Token);
_connection = await ConnectInternal(token);
_commandConnection = await ConnectInternal(token);
}
catch (MpcNET.Exceptions.MpcConnectException e)
{
@ -250,14 +217,11 @@ namespace unison
await UpdateStatusAsync();
await UpdateSongAsync();
Loop(_cancelCommand.Token);
Loop(token);
}
private async Task<MpcConnection> ConnectInternal(CancellationToken token)
{
if (token.IsCancellationRequested)
return null;
IPAddress.TryParse(Properties.Settings.Default.mpd_host, out _ipAddress);
if (_ipAddress == null)
@ -297,6 +261,18 @@ namespace unison
return connection;
}
public void Disconnected()
{
_connected = false;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
if (_connection != null)
_connection = null;
if (_commandConnection != null)
_commandConnection = null;
}
private void Loop(CancellationToken token)
{
Task.Run(async () =>
@ -305,7 +281,8 @@ namespace unison
{
try
{
if (token.IsCancellationRequested || _connection == null || !IsConnected())
token.ThrowIfCancellationRequested();
if (token.IsCancellationRequested || _connection == null)
break;
IMpdMessage<string> idleChanges = await _connection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update"));
@ -314,17 +291,14 @@ namespace unison
await HandleIdleResponseAsync(idleChanges.Response.Content);
else
{
Trace.WriteLine($"Error in Idle connection thread (1): {idleChanges.Response?.Content}");
Trace.WriteLine($"Error in Idle connection thread: {idleChanges.Response?.Content}");
throw new Exception(idleChanges.Response?.Content);
}
}
catch (Exception e)
{
if (token.IsCancellationRequested)
Trace.WriteLine($"Idle connection cancelled.");
else
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
await Initialize();
Disconnected();
break;
}
}
@ -347,7 +321,6 @@ namespace unison
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
await Initialize();
}
}
@ -372,7 +345,6 @@ namespace unison
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
await Initialize();
}
_isUpdatingStatus = false;
@ -380,8 +352,6 @@ namespace unison
private async Task UpdateSongAsync()
{
Trace.WriteLine("Updating song");
if (_connection == null || _isUpdatingSong)
return;
@ -401,17 +371,13 @@ namespace unison
catch (Exception e)
{
Trace.WriteLine($"Error in Idle connection thread: {e.Message}");
await Initialize();
}
_isUpdatingSong = false;
}
private async void GetAlbumCover(string path, CancellationToken token)
private async void GetAlbumCover(string path, CancellationToken token = default)
{
if (token.IsCancellationRequested)
return;
List<byte> data = new List<byte>();
try
{
@ -424,7 +390,7 @@ namespace unison
if (_connection == null)
return;
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new ReadPictureCommand(path, currentSize));
if (!albumReq.IsResponseValid)
break;
@ -446,7 +412,7 @@ namespace unison
if (_connection == null)
return;
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new ReadPictureCommand(path, currentSize));
IMpdMessage<MpdBinaryData> albumReq = await _connection.SendAsync(new AlbumArtCommand(path, currentSize));
if (!albumReq.IsResponseValid)
break;
@ -462,9 +428,6 @@ namespace unison
}
catch (Exception e)
{
if (token.IsCancellationRequested)
return;
Trace.WriteLine("Exception caught while getting albumart: " + e);
return;
}
@ -478,7 +441,7 @@ namespace unison
{
_cover = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
}
catch
catch (System.NotSupportedException)
{
_cover = null;
}
@ -513,7 +476,7 @@ namespace unison
SongChanged?.Invoke(this, EventArgs.Empty);
string uri = Regex.Escape(_currentSong.Path);
GetAlbumCover(uri, _cancelCommand.Token);
GetAlbumCover(uri);
}
public void UpdateCover()
@ -590,25 +553,24 @@ namespace unison
public async void QueryStats()
{
Dictionary<string, string> response = await SafelySendCommandAsync(new StatsCommand());
Dictionary<string, string> Response = await SafelySendCommandAsync(new StatsCommand());
if (Response == null)
return;
if (response != null)
{
_stats.Songs = int.Parse(response["songs"]);
_stats.Albums = int.Parse(response["albums"]);
_stats.Artists = int.Parse(response["artists"]);
_stats.Songs = int.Parse(Response["songs"]);
_stats.Albums = int.Parse(Response["albums"]);
_stats.Artists = int.Parse(Response["artists"]);
TimeSpan time;
time = TimeSpan.FromSeconds(int.Parse(response["uptime"]));
time = TimeSpan.FromSeconds(int.Parse(Response["uptime"]));
_stats.Uptime = time.ToString(@"dd\:hh\:mm\:ss");
time = TimeSpan.FromSeconds(int.Parse(response["db_playtime"]));
time = TimeSpan.FromSeconds(int.Parse(Response["db_playtime"]));
_stats.TotalPlaytime = time.ToString(@"dd\:hh\:mm\:ss");
time = TimeSpan.FromSeconds(int.Parse(response["playtime"]));
time = TimeSpan.FromSeconds(int.Parse(Response["playtime"]));
_stats.TotalTimePlayed = time.ToString(@"dd\:hh\:mm\:ss");
DateTime date = new DateTime(1970, 1, 1).AddSeconds(int.Parse(response["db_update"])).ToLocalTime();
DateTime date = new DateTime(1970, 1, 1).AddSeconds(int.Parse(Response["db_update"])).ToLocalTime();
_stats.DatabaseUpdate = date.ToString("dd/MM/yyyy @ HH:mm");
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -104,21 +104,21 @@
<VisualBrush Visual="{Binding ElementName=mask}"/>
</StackPanel.OpacityMask>
<Image x:Name="Cover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" />
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" Visibility="Collapsed" />
<Image x:Name="RadioCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/radio.png" Visibility="Collapsed" />
<Image x:Name="NoCover" HorizontalAlignment="Center" VerticalAlignment="Center" Source="/Resources/nocover.png" />
</StackPanel>
</Grid>
</Border>
</Grid>
<Grid x:Name="BottomLayout" HorizontalAlignment="Stretch" VerticalAlignment="Bottom" Background="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}" Width="Auto" MinHeight="40">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="10,0,0,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal" VerticalAlignment="Center" Margin="10,0,0,0">
<Button x:Name="Shuffle" Padding="5, 2" Click="Shuffle_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" Margin="0,0,10,0">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
</StackPanel>
</Button>
<Button x:Name="Radio" Padding="5, 2" HorizontalAlignment="Left" Click="Radios_Clicked" Margin="5,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
<Button x:Name="Radio" Padding="5, 2" HorizontalAlignment="Left" Click="Radios_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="📻" Padding="0,0,0,2"/>
<TextBlock Text="{x:Static properties:Resources.Radios}" Margin="5, 0, 0, 0"/>
@ -136,12 +136,13 @@
<TextBlock x:Name="Connection" HorizontalAlignment="Center" Text="Not connected" TextWrapping="Wrap" VerticalAlignment="Top" TextAlignment="Center" Foreground="{DynamicResource {x:Static SystemColors.ControlDarkDarkBrushKey}}" Margin="5,0,0,0" />
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,10,0">
<!--<Button x:Name="Shuffle" Padding="5, 2" HorizontalAlignment="Right" Margin="0,0,10,0" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}">
<Button x:Name="Snapcast" HorizontalAlignment="Left" VerticalAlignment="Center" Click="Snapcast_Clicked" Margin="0,0,10,0" Padding="5, 2" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}" IsEnabled="False">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🔁" Padding="0,0,0,2"/>
<TextBlock Text="Shuffle" Margin="5, 0, 0, 0"/>
<emoji:TextBlock Text="🔊" Padding="0,0,0,2"/>
<TextBlock x:Name="SnapcastText" Text="{x:Static properties:Resources.StartSnapcast}" Margin="5, 0, 0, 0"/>
</StackPanel>
</Button>-->
</Button>
<Button x:Name="Settings" Padding="5, 2" Click="Settings_Clicked" Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" FocusVisualStyle="{x:Null}">
<StackPanel Orientation="Horizontal">
<emoji:TextBlock Text="🛠️" Padding="0,0,0,2"/>

View File

@ -6,6 +6,8 @@ using System.Windows.Threading;
using System.Windows.Interop;
using System.Windows.Input;
using System.Windows.Controls.Primitives;
using MpcNET.Commands.Queue;
using System.Diagnostics;
namespace unison
{
@ -13,6 +15,7 @@ namespace unison
{
private readonly Settings _settingsWin;
private readonly Radios _radiosWin;
private readonly Shuffle _shuffleWin;
private readonly DispatcherTimer _timer;
private readonly MPDHandler _mpd;
@ -25,6 +28,7 @@ namespace unison
_settingsWin = new Settings();
_radiosWin = new Radios();
_shuffleWin = new Shuffle();
_timer = new DispatcherTimer();
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
@ -46,12 +50,12 @@ namespace unison
{
if (_mpd.IsConnected())
{
Snapcast.IsEnabled = true;
ConnectionOkIcon.Visibility = Visibility.Visible;
ConnectionFailIcon.Visibility = Visibility.Collapsed;
Snapcast.IsEnabled = true;
if (_radiosWin.IsConnected())
Radio.IsEnabled = true;
_shuffleWin.ListGenre();
_shuffleWin.ListFolder();
}
else
{
@ -59,15 +63,12 @@ namespace unison
DefaultState(true);
ConnectionOkIcon.Visibility = Visibility.Collapsed;
ConnectionFailIcon.Visibility = Visibility.Visible;
Snapcast.IsEnabled = false;
Radio.IsEnabled = false;
}
_settingsWin.UpdateConnectionStatus();
Connection.Text = $"{Properties.Settings.Default.mpd_host}:{Properties.Settings.Default.mpd_port}";
}
public void OnSongChanged(object sender, EventArgs e)
public async void OnSongChanged(object sender, EventArgs e)
{
if (_mpd.GetCurrentSong() == null)
return;
@ -113,6 +114,32 @@ namespace unison
_timer.Start();
EndTime.Text = FormatSeconds(_mpd.GetCurrentSong().Time);
}
Debug.WriteLine("Song changed called!");
// handle continuous shuffle
if (_shuffleWin.GetContinuous())
{
System.Collections.Generic.IEnumerable<MpcNET.Types.IMpdFile> a = await _mpd.SafelySendCommandAsync(new PlaylistCommand());
int queueSize = 0;
foreach (var i in a)
{
Debug.WriteLine(i.Path);
queueSize++;
}
Debug.WriteLine("queue size is: " + queueSize);
if (queueSize < 5)
{
_shuffleWin.AddContinuousSongs();
}
// query queue
// if (queue.SongRemaining < 5)
//{
// // query shuffle songs
//}
}
}
public void OnStatusChanged(object sender, EventArgs e)
@ -159,9 +186,8 @@ namespace unison
PlayPause.Text = (string)Application.Current.FindResource("pauseButton");
TimeSlider.Value = 50;
TimeSlider.IsEnabled = false;
NoCover.Visibility = Visibility.Collapsed;
NoCover.Visibility = Visibility.Visible;
Cover.Visibility = Visibility.Collapsed;
RadioCover.Visibility = Visibility.Collapsed;
if (LostConnection)
{
@ -173,18 +199,16 @@ namespace unison
public void OnCoverChanged(object sender, EventArgs e)
{
NoCover.Visibility = Visibility.Collapsed;
Cover.Visibility = Visibility.Collapsed;
RadioCover.Visibility = Visibility.Collapsed;
if (_mpd.GetCurrentSong().Time == -1)
RadioCover.Visibility = Visibility.Visible;
else if (_mpd.GetCover() == null)
if (_mpd.GetCover() == null)
{
NoCover.Visibility = Visibility.Visible;
Cover.Visibility = Visibility.Collapsed;
}
else if (Cover.Source != _mpd.GetCover())
{
Cover.Source = _mpd.GetCover();
Cover.Visibility = Visibility.Visible;
NoCover.Visibility = Visibility.Collapsed;
}
}
@ -197,6 +221,11 @@ namespace unison
SnapcastText.Text = unison.Resources.Resources.StartSnapcast;
}
public void OnRadioBrowserConnected()
{
Radio.IsEnabled = true;
}
public void UpdateButton(ref Border border, bool b)
{
border.Style = b ? (Style)Resources["SelectedButton"] : (Style)Resources["UnselectedButton"];
@ -238,6 +267,15 @@ namespace unison
_radiosWin.WindowState = WindowState.Normal;
}
public void Shuffle_Clicked(object sender, RoutedEventArgs e)
{
_shuffleWin.Show();
_shuffleWin.Activate();
if (_shuffleWin.WindowState == WindowState.Minimized)
_shuffleWin.WindowState = WindowState.Normal;
}
public void Settings_Clicked(object sender, RoutedEventArgs e)
{
_settingsWin.Show();

View File

@ -54,9 +54,6 @@ namespace unison
{
private RadioBrowserClient _radioBrowser;
private MPDHandler _mpd;
private bool _connected = true;
public bool IsConnected() => _connected;
public Radios()
{
@ -72,8 +69,11 @@ namespace unison
Debug.WriteLine("Exception while connecting to RadioBrowser: " + e.Message);
return;
}
_connected = true;
Application.Current.Dispatcher.Invoke(() =>
{
MainWindow MainWin = (MainWindow)Application.Current.MainWindow;
MainWin.OnRadioBrowserConnected();
});
}
public async void Initialize()

View File

@ -43,7 +43,7 @@
<StackPanel Margin="0,5,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_Password}" TextWrapping="Wrap" Margin="5,0,0,0"/>
<PasswordBox x:Name="MpdPassword" KeyDown="ConnectHandler" Width="250" Margin="10,2,0,0"/>
<PasswordBox x:Name="MpdPassword" Width="250" Margin="10,2,0,0"/>
<TextBlock Text="{x:Static properties:Resources.Settings_ConnectionPasswordInfo}" TextWrapping="Wrap" Margin="10,5,0,0" MaxWidth="250" HorizontalAlignment="Left"/>
</StackPanel>
@ -52,6 +52,7 @@
<Run x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}"/>
</TextBlock>
<!--<TextBlock x:Name="ConnectionStatus" Text="{x:Static properties:Resources.Settings_ConnectionStatusOffline}" TextWrapping="Wrap" Margin="5,10,0,0"/>-->
<Button x:Name="ConnectButton" Content="{x:Static properties:Resources.Settings_ConnectButton}" Margin="0,10,0,0" Width="120" Click="MPDConnect_Clicked"/>
</StackPanel>
</Grid>
@ -59,36 +60,6 @@
</DockPanel>
</TabItem>
<TabItem Header="Snapcast">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<emoji:TextBlock Text="🔊 Snapcast"/>
</GroupBox.Header>
<Grid VerticalAlignment="Top">
<StackPanel>
<StackPanel>
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
</CheckBox>
<CheckBox x:Name="SnapcastWindow" Margin="5,2.5,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastWindow}" TextWrapping="Wrap"/>
</CheckBox>
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPort}" TextWrapping="Wrap" Margin="5,5,0,0"/>
<TextBox x:Name="SnapcastPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPath}" TextWrapping="Wrap" Margin="5,5,0,0"/>
<TextBox x:Name="SnapcastPath" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
<TextBlock TextWrapping="Wrap" Margin="5,5,0,0" TextAlignment="Left" Width="250">
<Run Text="{x:Static properties:Resources.Settings_SnapcastInfo1}" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo2}" FontStyle="Italic" FontWeight="DemiBold" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo3}" />
</TextBlock>
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
</StackPanel>
</StackPanel>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="{x:Static properties:Resources.Settings_Shortcuts}">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
@ -190,7 +161,67 @@
</StackPanel>
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="ShortcutsReset_Clicked"/>
</StackPanel>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="Snapcast">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<emoji:TextBlock Text="🔊 Snapcast"/>
</GroupBox.Header>
<Grid VerticalAlignment="Top">
<StackPanel>
<StackPanel>
<CheckBox x:Name="SnapcastStartup" Margin="5, 5, 0, 0">
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastLauch}" TextWrapping="Wrap"/>
</CheckBox>
<CheckBox x:Name="SnapcastWindow" Margin="5,2.5,0,0">
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastWindow}" TextWrapping="Wrap"/>
</CheckBox>
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPort}" TextWrapping="Wrap" Margin="5,5,0,0"/>
<TextBox x:Name="SnapcastPort" MaxLength="5" PreviewTextInput="NumberValidationTextBox" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
<TextBlock Text="{x:Static properties:Resources.Settings_SnapcastPath}" TextWrapping="Wrap" Margin="5,5,0,0"/>
<TextBox x:Name="SnapcastPath" TextWrapping="Wrap" Width="250" Margin="10,2,5,0" HorizontalAlignment="Left"/>
<TextBlock TextWrapping="Wrap" Margin="5,5,0,0" TextAlignment="Left" Width="250">
<Run Text="{x:Static properties:Resources.Settings_SnapcastInfo1}" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo2}" FontStyle="Italic" FontWeight="DemiBold" /><Run Text="{x:Static properties:Resources.Settings_SnapcastInfo3}" />
</TextBlock>
<Button Content="{x:Static properties:Resources.Settings_SnapcastResetButton}" Margin="0,10,0,0" Width="120" Click="SnapcastReset_Clicked"/>
</StackPanel>
</StackPanel>
</Grid>
</GroupBox>
</DockPanel>
</TabItem>
<TabItem Header="Shuffle">
<DockPanel Margin="8">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text="🔁 "/>
<Run Text="Shuffle"></Run>
</TextBlock>
</GroupBox.Header>
<Grid MaxWidth="500">
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox TextWrapping="Wrap" Width="25" PreviewTextInput="NumberValidationTextBox" Margin="0,2,0,0"/>
<TextBlock Text="Prevent repetition rate (0-100%)" TextWrapping="Wrap" Margin="5,2,0,0"/>
</StackPanel>
<TextBlock TextWrapping="Wrap" Margin="0,10,0,0">
<Run>The shuffle window allows to add random songs to your queue. Both options take into account the filter.</Run>
<Run>If the filter is empty, the entire music library is taken into account.</Run><LineBreak/><LineBreak/>
<Run FontWeight="Bold">Continuous shuffle</Run><LineBreak/>
<Run>By enabling this option, unison will automatically add songs to the queue so you never run out of songs to listen to.</Run>
<LineBreak/><LineBreak/>
<Run FontWeight="Bold">Add to queue</Run><LineBreak/>
<Run>Add a fixed number of songs to the queue. It can take a long time to add more than 100 songs, so the option is limited to 1000 songs.</Run>
</TextBlock>
</StackPanel>
</Grid>
</GroupBox>

View File

@ -111,11 +111,14 @@ namespace unison
SaveSettings();
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (mpd.IsConnected())
mpd = new MPDHandler();
ConnectButton.IsEnabled = false;
ConnectionStatus.Text = unison.Resources.Resources.Settings_ConnectionStatusConnecting;
MPDHandler mpd = (MPDHandler)Application.Current.Properties["mpd"];
System.Threading.Tasks.Task.Run(async () => { await mpd.Initialize(); });
System.Threading.Tasks.Task.Run(() => { mpd.Connect(); });
}
private void SnapcastReset_Clicked(object sender, RoutedEventArgs e)

102
Views/Shuffle.xaml Normal file
View File

@ -0,0 +1,102 @@
<Window x:Class="unison.Shuffle"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:emoji="clr-namespace:Emoji.Wpf;assembly=Emoji.Wpf"
xmlns:local="clr-namespace:unison"
mc:Ignorable="d"
Title="Shuffle" Closing="Window_Closing" SizeToContent="WidthAndHeight" ResizeMode="NoResize">
<Grid>
<StackPanel>
<StackPanel HorizontalAlignment="Left" Orientation="Vertical" Margin="5,0,5,5">
<GroupBox DockPanel.Dock="Top" Padding="0,4,0,0">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text="🔡"/>
<Run Text="Filter"/>
</TextBlock>
</GroupBox.Header>
<StackPanel Orientation="Vertical" Margin="5,0,5,0">
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Vertical">
<TextBlock Text="Song" Margin="0,0,0,2"/>
<TextBox x:Name="Song" Width="240" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="20,0,0,0">
<TextBlock Text="Artist" Margin="0,0,0,2"/>
<TextBox x:Name="Artist" Width="240" Margin="5,0,0,0"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,5,0,0">
<StackPanel Orientation="Vertical">
<TextBlock Text="Album" Margin="0,0,0,2"/>
<TextBox x:Name="Album" Width="240" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="20,0,0,0">
<TextBlock Text="Year" Margin="0,0,0,2"/>
<TextBox x:Name="Year" Width="240" Margin="5,0,0,0"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,5,0,5">
<StackPanel Orientation="Vertical">
<TextBlock Text="Genre" Margin="0,0,0,2"/>
<ComboBox x:Name="Genre" SelectedIndex="0" Width="240" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" FocusVisualStyle="{x:Null}"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="20,0,0,0">
<TextBlock Text="Directory" Margin="0,0,0,2" TextDecorations="{x:Null}"/>
<ComboBox x:Name="Directory" SelectedIndex="0" Width="240" ScrollViewer.CanContentScroll="False" Margin="5,0,0,0" IsEnabled="True"/>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,5,0,5">
<Button Content="Reset" Click="Reset_Clicked" Padding="5, 2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
</StackPanel>
<StackPanel x:Name="SongFilterPanel" Margin="0,5,0,0" Visibility="Collapsed">
<TextBlock>
<Run Text="Number of songs in filter: "/><Run x:Name="SongFilterNumber" FontWeight="Bold"/>
</TextBlock>
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox Margin="0,5,0,0" HorizontalAlignment="Stretch">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text="♾️"/>
<Run Text="Continuous shuffle"/>
</TextBlock>
</GroupBox.Header>
<StackPanel Orientation="Horizontal" Margin="5,8,0,0">
<CheckBox x:Name="ContinuousShuffle" Checked="ContinuousShuffle_Checked" Unchecked="ContinuousShuffle_Checked" FocusVisualStyle="{x:Null}">
<TextBlock Text="Enable continuous shuffle" TextWrapping="Wrap"/>
</CheckBox>
</StackPanel>
</GroupBox>
<GroupBox x:Name="AddToQueueGroup" Margin="0,5,0,0" HorizontalAlignment="Stretch">
<GroupBox.Header>
<TextBlock>
<emoji:EmojiInline Text=""/>
<Run Text="Add to queue"/>
</TextBlock>
</GroupBox.Header>
<StackPanel Margin="5,0,0,0">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,10,0,0">
<TextBox x:Name="SongNumber" Text="100" Width="35" HorizontalAlignment="Left" VerticalAlignment="Top"/>
<TextBlock Text="Number of songs to add" Margin="5,0,0,5"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
<Button Content="Add to queue" Click="AddToQueue_Clicked" Padding="5, 2" HorizontalAlignment="Left" FocusVisualStyle="{x:Null}"/>
<TextBlock x:Name="SearchStatus" Margin="15,3,0,0" FontStyle="Italic" Visibility="Collapsed">
<Run Text="Added "/><Run x:Name="NumberAddedSongs"/><Run Text=" songs to the queue..."/>
</TextBlock>
</StackPanel>
</StackPanel>
</GroupBox>
</StackPanel>
</StackPanel>
</Grid>
</Window>

307
Views/Shuffle.xaml.cs Normal file
View File

@ -0,0 +1,307 @@
using MpcNET;
using MpcNET.Commands.Database;
using MpcNET.Commands.Reflection;
using MpcNET.Tags;
using MpcNET.Types;
using MpcNET.Types.Filters;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
namespace unison
{
public partial class Shuffle : Window
{
private MPDHandler _mpd;
bool _continuous = false;
List<string> _songList { get; }
public Shuffle()
{
InitializeComponent();
_songList = new();
}
private bool IsFilterEmpty()
{
if (Song.Text.Length == 0 && Artist.Text.Length == 0 && Album.Text.Length == 0 && Year.Text.Length == 0 && Genre.SelectedIndex == 0 && Directory.Text.Length == 0)
return true;
return false;
}
public bool GetContinuous()
{
return _continuous;
}
public void AddContinuousSongs()
{
if (IsFilterEmpty())
{
// add a completely random song
ContinuousShuffle_AddToQueueRandom();
return;
}
int AddedSongs = 0;
NumberAddedSongs.Text = AddedSongs.ToString();
SearchStatus.Visibility = Visibility.Visible;
HashSet<int> SongIndex = new();
while (SongIndex.Count < 2)
{
int MaxIndex = new Random().Next(0, _songList.Count - 1);
SongIndex.Add(MaxIndex);
}
foreach (int index in SongIndex)
_mpd.AddSong(_songList[index]);
SearchStatus.Visibility = Visibility.Collapsed;
}
public async void ListGenre()
{
if (Genre.Items.Count == 0)
{
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
List<string> Response = await _mpd.SafelySendCommandAsync(new ListCommand(MpdTags.Genre, null, null));
if (Response.Count > 0)
{
Genre.Items.Add("");
foreach (var genre in Response)
Genre.Items.Add(genre);
}
}
}
public async void ListFolder()
{
if (Directory.Items.Count == 0)
{
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
IEnumerable<IMpdFilePath> Response = await _mpd.SafelySendCommandAsync(new LsInfoCommand(""));
if (Response != null)
{
Directory.Items.Add("");
foreach (var directory in Response)
Directory.Items.Add(directory.Name);
}
}
}
private void Window_Closing(object sender, CancelEventArgs e)
{
e.Cancel = true;
WindowState = WindowState.Minimized;
Hide();
}
public void InitHwnd()
{
WindowInteropHelper helper = new(this);
helper.EnsureHandle();
}
private void Reset_Clicked(object sender, RoutedEventArgs e)
{
Song.Text = "";
Artist.Text = "";
Album.Text = "";
Year.Text = "";
Genre.SelectedIndex = 0;
Directory.SelectedIndex = 0;
}
private async void ContinuousShuffle_Checked(object sender, RoutedEventArgs e)
{
if (ContinuousShuffle.IsChecked == true)
{
AddToQueueGroup.IsEnabled = false;
_continuous = true;
_songList.Clear();
if (!IsFilterEmpty())
{
/*await*/
GetSongsFromFilter();
}
}
else
{
AddToQueueGroup.IsEnabled = true;
_continuous = false;
}
}
private async void ContinuousShuffle_AddToQueueRandom()
{
for (int i = 0; i < 2; i++)
{
// generate random number
int song = new Random().Next(0, _mpd.GetStats().Songs - 1);
// query random song
CommandList commandList = new CommandList(new IMpcCommand<object>[] { new SearchCommand(MpdTags.Title, "", song, song + 1) });
string Response = await _mpd.SafelySendCommandAsync(commandList);
await Task.Delay(1);
if (Response.Length > 0)
{
// parse song and add it to queue
int start = Response.IndexOf("[file, ");
int end = Response.IndexOf("],");
string filePath = Response.Substring(start + 7, end - (start + 7));
_mpd.AddSong(filePath);
}
}
SearchStatus.Visibility = Visibility.Collapsed;
}
private async void AddToQueueRandom()
{
int AddedSongs = 0;
NumberAddedSongs.Text = AddedSongs.ToString();
SearchStatus.Visibility = Visibility.Visible;
for (int i = 0; i < int.Parse(SongNumber.Text); i++)
{
// generate random number
int song = new Random().Next(0, _mpd.GetStats().Songs - 1);
// query random song
CommandList commandList = new CommandList(new IMpcCommand<object>[] { new SearchCommand(MpdTags.Title, "", song, song + 1) });
string Response = await _mpd.SafelySendCommandAsync(commandList);
await Task.Delay(1);
if (Response.Length > 0)
{
// parse song and add it to queue
int start = Response.IndexOf("[file, ");
int end = Response.IndexOf("],");
string filePath = Response.Substring(start + 7, end - (start + 7));
_mpd.AddSong(filePath);
AddedSongs++;
NumberAddedSongs.Text = AddedSongs.ToString();
}
}
SearchStatus.Visibility = Visibility.Collapsed;
}
private async Task GetSongsFromFilter()
{
_songList.Clear();
SongFilterPanel.Visibility = Visibility.Visible;
int song = _mpd.GetStats().Songs;
List<IFilter> filtersA = new();
if (Song.Text != "")
filtersA.Add(new FilterTag(MpdTags.Title, Song.Text, FilterOperator.Contains));
if (Artist.Text != "")
filtersA.Add(new FilterTag(MpdTags.Artist, Artist.Text, FilterOperator.Contains));
if (Album.Text != "")
filtersA.Add(new FilterTag(MpdTags.Album, Album.Text, FilterOperator.Contains));
if (Year.Text != "")
filtersA.Add(new FilterTag(MpdTags.Date, Year.Text, FilterOperator.Contains));
if (Genre.Text != "")
filtersA.Add(new FilterTag(MpdTags.Genre, Genre.Text, FilterOperator.Contains));
if (Directory.Text != "")
filtersA.Add(new FilterBase(Directory.Text, FilterOperator.None));
Debug.WriteLine(Directory.Text);
CommandList commandList = new CommandList(new IMpcCommand<object>[] { new SearchCommand(filtersA, 0, song + 1) });
string Response = await _mpd.SafelySendCommandAsync(commandList);
Debug.WriteLine(Response);
// create a list of the file url
string[] value = Response.Split(", [file, ");
// there are no song in this filter
if (value[0] == "")
{
SongFilterNumber.Text = _songList.Count.ToString();
return;
}
foreach (string file in value)
{
int start = 0;
int end = file.IndexOf("],");
string filePath = file.Substring(start, end - start);
Debug.WriteLine(filePath);
_songList.Add(filePath);
SongFilterNumber.Text = _songList.Count.ToString();
}
// remove characters from first file
_songList[0] = _songList[0].Substring(7, _songList[0].Length - 7);
SongFilterPanel.Visibility = Visibility.Visible;
SongFilterNumber.Text = _songList.Count.ToString();
}
private async void AddToQueueFilter()
{
await GetSongsFromFilter();
int AddedSongs = 0;
NumberAddedSongs.Text = AddedSongs.ToString();
SearchStatus.Visibility = Visibility.Visible;
// more requested songs than available => add everything
if (int.Parse(SongNumber.Text) > _songList.Count)
{
foreach (string path in _songList)
{
await Task.Delay(1);
_mpd.AddSong(path);
AddedSongs++;
NumberAddedSongs.Text = AddedSongs.ToString();
}
}
// more available songs than requested =>
// we add unique indexes until we reach the requested amount
else
{
HashSet<int> SongIndex = new();
while (SongIndex.Count < int.Parse(SongNumber.Text))
{
int MaxIndex = new Random().Next(0, _songList.Count - 1);
SongIndex.Add(MaxIndex);
}
foreach (int index in SongIndex)
_mpd.AddSong(_songList[index]);
}
SearchStatus.Visibility = Visibility.Collapsed;
}
private void AddToQueue_Clicked(object sender, RoutedEventArgs e)
{
_mpd = (MPDHandler)Application.Current.Properties["mpd"];
if (_mpd.GetStats() == null)
return;
if (IsFilterEmpty())
AddToQueueRandom();
else
AddToQueueFilter();
}
}
}

View File

@ -30,11 +30,11 @@
<Image Width="16" Height="16" emoji:Image.Source="📻" />
</MenuItem.Icon>
</MenuItem>
<!--<MenuItem Header="Shuffle" Command="{Binding Shuffle}">
<MenuItem Header="Shuffle" Command="{Binding Shuffle}">
<MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="🔀" />
</MenuItem.Icon>
</MenuItem>-->
</MenuItem>
<MenuItem Header="{x:Static properties:Resources.Settings}" Command="{Binding Settings}">
<MenuItem.Icon>
<Image Width="16" Height="16" emoji:Image.Source="🛠️" />

View File

@ -71,6 +71,18 @@ namespace unison
}
}
public ICommand Shuffle
{
get
{
return new DelegateCommand
{
CommandAction = () => ((MainWindow)Application.Current.MainWindow).Shuffle_Clicked(null, null),
CanExecuteFunc = () => true
};
}
}
public ICommand Settings
{
get

View File

@ -2,17 +2,17 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ApplicationIcon>Resources\icon-full.ico</ApplicationIcon>
<Win32Resource></Win32Resource>
<StartupObject>unison.App</StartupObject>
<Version>1.3.1</Version>
<Version>1.3</Version>
<Company />
<Authors>Théo Marchal</Authors>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageProjectUrl>https://github.com/ZetaKebab/unison</PackageProjectUrl>
<RepositoryUrl>https://github.com/ZetaKebab/unison</RepositoryUrl>
<PackageProjectUrl>https://git.n700.ovh/keb/unison</PackageProjectUrl>
<RepositoryUrl>https://git.n700.ovh/keb/unison</RepositoryUrl>
<Copyright>Théo Marchal</Copyright>
<PackageIconUrl />
</PropertyGroup>
@ -22,8 +22,6 @@
<None Remove="Resources\icon-mini.ico" />
<None Remove="Resources\nocover.png" />
<None Remove="LICENSE" />
<None Remove="Resources\nothing.png" />
<None Remove="Resources\radio.png" />
<None Include="LICENSE">
<Pack>True</Pack>
<PackagePath></PackagePath>
@ -40,12 +38,6 @@
<Resource Include="Resources\nocover.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\nothing.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\radio.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Content Include="LICENSE">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>