diff --git a/SynAudio/App.xaml.cs b/SynAudio/App.xaml.cs index 3a7f4e2..38b6280 100644 --- a/SynAudio/App.xaml.cs +++ b/SynAudio/App.xaml.cs @@ -34,7 +34,7 @@ public partial class App : Application internal static readonly string UserDataFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), nameof(SynAudio)); #endif - internal static readonly Encryption.Encrypter Encrypter = new Encryption.Encrypter("2BE93913-B573-4DE5-8CCA-9BC14FA41201", Encoding.UTF8.GetBytes("35FE3A9B-227A-4184-8426-3765669C12F8")); + internal static readonly Encryption.Encrypter Encrypter = new Encryption.Encrypter("833236b9e38f36c240fba48a48d2a160185671cc08a9d4fef75cc8b33e4166cd", Encoding.UTF8.GetBytes($"{UserDataFolder}-{Environment.UserDomainName}-{Environment.UserName}")); internal static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings() { @@ -76,21 +76,27 @@ internal static void RefreshCommands() Current.Dispatcher.BeginInvoke(new Action(() => System.Windows.Input.CommandManager.InvalidateRequerySuggested())); } - internal static string GetNasFileFullUncPath(string internalPath) => NetworkHelper.GetUncPath(Settings.Connection.MusicFolderPath, internalPath); + //internal static string GetNasFileFullUncPath(string internalPath) => NetworkHelper.GetUncPath(MusicFolderPath, internalPath); internal static bool ExistsOnHost(string path, out string uncPath) { uncPath = null; - if (MusicFolderAvailableOnLan) - { - uncPath = GetNasFileFullUncPath(path); - return File.Exists(uncPath); - } + // Todo + //if (MusicFolderAvailableOnLan) + //{ + // uncPath = GetNasFileFullUncPath(path); + // return File.Exists(uncPath); + //} return false; } internal static SqlCeLibrary.SqlCe GetSql() => new SqlCeLibrary.SqlCe(LibraryDatabaseFile, false); + internal static void SaveSettings() + { + Storage.Save(nameof(Settings), Settings); + } + protected override void OnStartup(StartupEventArgs e) { _mutex = new Mutex(true, MutexName, out var createdNewMutex); @@ -165,7 +171,6 @@ protected override void OnStartup(StartupEventArgs e) if (!Storage.TryLoad(nameof(Settings), out var settings)) settings = new SettingsModel(); Settings = settings; - MusicFolderAvailableOnLan = !string.IsNullOrWhiteSpace(Settings.Connection.MusicFolderPath) && Directory.Exists(Settings.Connection.MusicFolderPath); // Catch binding errors PresentationTraceSources.Refresh(); @@ -196,7 +201,7 @@ private void App_DispatcherUnhandledException(object sender, System.Windows.Thre protected override void OnExit(ExitEventArgs e) { _log.Info(nameof(OnExit)); - Storage.Save(nameof(Settings), Settings); + SaveSettings(); NLog.LogManager.Shutdown(); if (_mutex != null) _mutex.ReleaseMutex(); diff --git a/SynAudio/DAL/ByteArrayValue.cs b/SynAudio/DAL/ByteArrayValue.cs new file mode 100644 index 0000000..3d0bc11 --- /dev/null +++ b/SynAudio/DAL/ByteArrayValue.cs @@ -0,0 +1,27 @@ +using SqlCeLibrary; + +namespace SynAudio.DAL +{ + [Table("ByteArray")] + public class ByteArrayValue + { + [Column, PrimaryKey] + public string Key { get; set; } + [Column] + public byte[] Value { get; set; } + + public static byte[] Read(SqlCe sql, string key) => sql.SelectFirstByPrimaryKeys(key)?.Value; + public static bool TryRead(SqlCe sql, string key, out byte[] value) + { + var v = sql.SelectFirstByPrimaryKeys(key); + value = v?.Value; + return !(value is null); + } + public static void Write(SqlCe sql, string key, byte[] value) + { + sql.DeleteSingleByPrimaryKey(key); + if (value?.Length > 0 == true) + sql.Insert(new ByteArrayValue() { Key = key, Value = value }); + } + } +} diff --git a/SynAudio/DAL/ByteArrayValues.cs b/SynAudio/DAL/ByteArrayValues.cs new file mode 100644 index 0000000..ce8bf8d --- /dev/null +++ b/SynAudio/DAL/ByteArrayValues.cs @@ -0,0 +1,7 @@ +namespace SynAudio.DAL +{ + public enum ByteArrayValues + { + AudioStationConnectorSession + } +} diff --git a/SynAudio/DAL/StringValue.cs b/SynAudio/DAL/StringValue.cs index b93003e..5a4e579 100644 --- a/SynAudio/DAL/StringValue.cs +++ b/SynAudio/DAL/StringValue.cs @@ -7,6 +7,7 @@ class StringValue { [Column, PrimaryKey] public string Key { get; set; } + [Column(Size = "1000"), NotNull] public string Value { get; set; } diff --git a/SynAudio/DAL/StringValues.cs b/SynAudio/DAL/StringValues.cs index 848c232..61d836d 100644 --- a/SynAudio/DAL/StringValues.cs +++ b/SynAudio/DAL/StringValues.cs @@ -2,7 +2,6 @@ { public enum StringValues { - AudioStationConnectorSession, NowPlaying_CurrentSongId } } diff --git a/SynAudio/Extensions/SqlCeExtensions.cs b/SynAudio/Extensions/SqlCeExtensions.cs index 105b816..3ecc0d9 100644 --- a/SynAudio/Extensions/SqlCeExtensions.cs +++ b/SynAudio/Extensions/SqlCeExtensions.cs @@ -16,5 +16,11 @@ static class SqlCeExtensions public static bool TryReadString(this SqlCe sql, StringValues key, out string value) => StringValue.TryRead(sql, key.ToString(), out value); public static void WriteString(this SqlCe sql, StringValues key, string value) => StringValue.Write(sql, key.ToString(), value); #endregion + + #region BlobValue + public static byte[] ReadBlob(this SqlCe sql, ByteArrayValues key) => ByteArrayValue.Read(sql, key.ToString()); + public static bool TryReadBlob(this SqlCe sql, ByteArrayValues key, out byte[] value) => ByteArrayValue.TryRead(sql, key.ToString(), out value); + public static void WriteBlob(this SqlCe sql, ByteArrayValues key, byte[] value) => ByteArrayValue.Write(sql, key.ToString(), value); + #endregion } } diff --git a/SynAudio/Library/AudioLibrary.cs b/SynAudio/Library/AudioLibrary.cs index 60c2baf..6b027b5 100644 --- a/SynAudio/Library/AudioLibrary.cs +++ b/SynAudio/Library/AudioLibrary.cs @@ -1,12 +1,15 @@ using System; using System.IO; +using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; using SqlCeLibrary; using SynAudio.DAL; using SynAudio.Library.Exceptions; +using SynAudio.Models; using SynAudio.Models.Config; using SynAudio.Utils; +using SynCommon.Serialization; using SynologyDotNet; using SynologyDotNet.AudioStation; using SynologyDotNet.Core.Model; @@ -79,49 +82,47 @@ public AudioLibrary(SettingsModel settings, ViewModels.StatusViewModel status) } } - public async Task ConnectAsync() + public async Task ConnectAsync(string password = null) { - _log.Debug(nameof(ConnectAsync)); - try + if (!(_audioStation is null)) { - if (!(_audioStation is null)) - { - _audioStation.Dispose(); - _audioStation = null; - } + _audioStation.Dispose(); + _audioStation = null; + } - if (string.IsNullOrWhiteSpace(Settings.Connection.Url)) - throw new NullReferenceException("API url is null"); + if (string.IsNullOrWhiteSpace(Settings.Url)) + throw new NullReferenceException("API url is null"); - _audioStation = new AudioStationClient(); - _synoClient = new SynoClient(new Uri(Settings.Connection.Url), true, _audioStation); + _audioStation = new AudioStationClient(); + _synoClient = new SynoClient(new Uri(Settings.Url), true, _audioStation); - using (var sql = Sql()) + using (var sql = Sql()) + { + // Login + SynoSession session = null; + if (password is null && sql.TryReadBlob(ByteArrayValues.AudioStationConnectorSession, out var encryptedSession)) { - // Login - SynoSession session; - if (sql.TryReadString(StringValues.AudioStationConnectorSession, out var json)) - { - session = JsonConvert.DeserializeObject(json); - await _synoClient.LoginWithPreviousSessionAsync(session, false); - } - else - { - session = await _synoClient.LoginAsync(Settings.Connection.Username, App.Encrypter.Decrypt(Settings.Connection.Password)).ConfigureAwait(false); - } - - // Test connection - var response = await _audioStation.ListSongsAsync(1, 0, SynologyDotNet.AudioStation.Model.SongQueryAdditional.None).ConfigureAwait(false); - if (!response.Success) - session = null; - sql.WriteString(StringValues.AudioStationConnectorSession, !(session is null) ? JsonConvert.SerializeObject(session) : null); - Connected = response.Success; + // Re-use session + session = JsonSerialization.DeserializeFromBytes(App.Encrypter.Decrypt(encryptedSession)); + await _synoClient.LoginWithPreviousSessionAsync(session, false); } - } - catch (Exception ex) - { - Connected = false; - _log.Error(ex); + else if (!(password is null)) + { + // Login with credentials + session = await _synoClient.LoginAsync(Settings.Username, password).ConfigureAwait(false); + } + else + { + // Must enter credentials + throw new System.Security.Authentication.AuthenticationException("Must enter crdentials."); + } + + // Test connection + var response = await _audioStation.ListSongsAsync(1, 0, SynologyDotNet.AudioStation.Model.SongQueryAdditional.None).ConfigureAwait(false); + if (!response.Success) + session = null; + sql.WriteBlob(ByteArrayValues.AudioStationConnectorSession, !(session is null) ? App.Encrypter.Encrypt(JsonSerialization.SerializeToBytes(session)) : null); + Connected = response.Success; } return Connected; } @@ -131,7 +132,7 @@ public void Logout() _log.Debug(nameof(Logout)); Connected = false; using (var sql = Sql()) - sql.WriteString(StringValues.AudioStationConnectorSession, null); + sql.WriteBlob(ByteArrayValues.AudioStationConnectorSession, null); } public void Dispose() diff --git a/SynAudio/Models/Config/ConnectionSettingsModel.cs b/SynAudio/Models/Config/ConnectionSettingsModel.cs deleted file mode 100644 index 7d6e3c7..0000000 --- a/SynAudio/Models/Config/ConnectionSettingsModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Windows.Documents; - -namespace SynAudio.Models.Config -{ - [Serializable] - public class ConnectionSettingsModel - { - /// - /// Url to the NAS, like "https://yourserver.example.com:5001" - /// Both HTTP and HTTPS endpoints are supported. - /// - public string Url { get; set; } - - /// - /// NAS user account name - /// - public string Username { get; set; } - - /// - /// NAS user password - /// - public string Password { get; set; } - - /// - /// Set this value if you can fetch files directly from your NAS - /// - public string MusicFolderPath { get; set; } - - public bool IsSet() => !(string.IsNullOrWhiteSpace(Url) || string.IsNullOrWhiteSpace(Username) || string.IsNullOrEmpty(Password)); - } -} diff --git a/SynAudio/Models/Config/SettingsModel.cs b/SynAudio/Models/Config/SettingsModel.cs index 2bdbe37..6d7c1a6 100644 --- a/SynAudio/Models/Config/SettingsModel.cs +++ b/SynAudio/Models/Config/SettingsModel.cs @@ -6,7 +6,17 @@ namespace SynAudio.Models.Config [Serializable] public class SettingsModel { - public ConnectionSettingsModel Connection { get; set; } = new ConnectionSettingsModel(); + /// + /// Url to the NAS, like "https://yourserver.example.com:5001" + /// Both HTTP and HTTPS endpoints are supported. + /// + public string Url { get; set; } + + /// + /// NAS user account name + /// + public string Username { get; set; } + public TranscodeMode Transcoding { get; set; } = TranscodeMode.WAV; public int Volume { get; set; } = 100; public System.Windows.WindowState WindowState { get; set; } = System.Windows.WindowState.Maximized; diff --git a/SynAudio/Models/Credentials.cs b/SynAudio/Models/Credentials.cs new file mode 100644 index 0000000..b14af5c --- /dev/null +++ b/SynAudio/Models/Credentials.cs @@ -0,0 +1,9 @@ +namespace SynAudio.Models +{ + public class Credentials + { + public string Url { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + } +} diff --git a/SynAudio/Properties/AssemblyInfo.cs b/SynAudio/Properties/AssemblyInfo.cs index 64ac37a..730def8 100644 --- a/SynAudio/Properties/AssemblyInfo.cs +++ b/SynAudio/Properties/AssemblyInfo.cs @@ -49,6 +49,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.3.0.0")] -[assembly: AssemblyFileVersion("0.3.0.0")] +[assembly: AssemblyVersion("0.3.1.0")] +[assembly: AssemblyFileVersion("0.3.1.0")] [assembly: Guid("127198AC-AB11-4077-B64A-B1618E5B231F")] diff --git a/SynAudio/SynAudio.csproj b/SynAudio/SynAudio.csproj index 92cd1b3..b3ad506 100644 --- a/SynAudio/SynAudio.csproj +++ b/SynAudio/SynAudio.csproj @@ -73,6 +73,8 @@ Designer + + @@ -133,12 +135,12 @@ - + @@ -149,7 +151,6 @@ - diff --git a/SynAudio/ViewModels/LoginDialogViewModel.cs b/SynAudio/ViewModels/LoginDialogViewModel.cs deleted file mode 100644 index 933a3db..0000000 --- a/SynAudio/ViewModels/LoginDialogViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using SynAudio.Models; - -namespace SynAudio.ViewModels -{ - public class LoginDialogViewModel : ViewModelBase - { - public string Url { get; set; } - public string MusicFolder { get; set; } - public string Username { get; set; } - public string Password { get; set; } - } -} diff --git a/SynAudio/ViewModels/MainWindowViewModel.cs b/SynAudio/ViewModels/MainWindowViewModel.cs index d3ea6c2..b4d0b1e 100644 --- a/SynAudio/ViewModels/MainWindowViewModel.cs +++ b/SynAudio/ViewModels/MainWindowViewModel.cs @@ -520,6 +520,12 @@ public void PlaySong(SongViewModel songVM) StartPlaypack(NowPlaying.CurrentSong); } + public void Disconnect() + { + Library.Logout(); + Application.Current.Shutdown(); + } + public void BackupUserData() { try @@ -610,36 +616,6 @@ private void SynchronizeLibraryCommand_Action(object o) } } - public bool ShowLoginDialog() - { - var vm = new LoginDialogViewModel() - { - Url = Settings.Connection.Url, - Username = Settings.Connection.Username, - MusicFolder = Settings.Connection.MusicFolderPath - }; - try - { - vm.Password = string.IsNullOrEmpty(Settings.Connection.Password) ? string.Empty : App.Encrypter.Decrypt(Settings.Connection.Password); - } - catch { } - - var ld = new LoginDialog(vm) - { - Owner = App.Current.MainWindow - }; - if (ld.ShowDialog() == true) - { - Settings.Connection.Url = ld.Result.Url; - Settings.Connection.MusicFolderPath = ld.Result.MusicFolder; - Settings.Connection.Username = ld.Result.Username; - Settings.Connection.Password = App.Encrypter.Encrypt(ld.Result.Password); - Library.Logout(); - return true; - } - return false; - } - public bool LibraryLoaded { get; set; } public async Task Open() @@ -793,32 +769,34 @@ private void Player_PlayerException(object sender, Exception e) private async Task TryToConnect() { - async Task ConnectLibrary() + Credentials credentials = null; // Try to re-use the session first + while (!Connected) { try { - if (!Settings.Connection.IsSet()) - throw new Exception("Please enter the connection parameters correctly!"); using (var cur = new CursorChange(Cursors.Wait)) - { - Connected = await Task.Run(async () => await Library.ConnectAsync()); - } + Connected = await Task.Run(async () => await Library.ConnectAsync(credentials?.Password)); } catch (Exception ex) { - Connected = false; - LogAndShowException(ex, "Could not connect to the server."); + _log.Error(ex); } - } - if (Settings.Connection.IsSet()) //Skip the first attempt if we already know that the login dialog has to be shown - await ConnectLibrary(); - while (!Connected) - { - if (ShowLoginDialog()) - await ConnectLibrary(); - else - Environment.Exit(0); + // Show login dialog + if (!Connected) + { + if (credentials is null) + credentials = new Credentials() + { + Url = Settings.Url, + Username = Settings.Username + }; + if (new LoginDialog(credentials).ShowDialog() != true) + Environment.Exit(0); + Settings.Url = credentials.Url; + Settings.Username = credentials.Username; + App.SaveSettings(); + } } App.RefreshCommands(); } diff --git a/SynAudio/Views/LoginDialog.xaml b/SynAudio/Views/LoginDialog.xaml index 2463011..3daae65 100644 --- a/SynAudio/Views/LoginDialog.xaml +++ b/SynAudio/Views/LoginDialog.xaml @@ -4,50 +4,50 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:SynAudio.Views" - xmlns:viewmodels="clr-namespace:SynAudio.ViewModels" + xmlns:models="clr-namespace:SynAudio.Models" mc:Ignorable="d" - d:DataContext="{d:DesignInstance viewmodels:LoginDialogViewModel}" + d:DataContext="{d:DesignInstance models:Credentials}" Title="Login" SizeToContent="Height" Width="400" WindowStartupLocation="CenterOwner"> - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - + - - + + - - - - - - - + + + + + + + - + diff --git a/SynAudio/Views/LoginDialog.xaml.cs b/SynAudio/Views/LoginDialog.xaml.cs index 5ae19de..a2f241e 100644 --- a/SynAudio/Views/LoginDialog.xaml.cs +++ b/SynAudio/Views/LoginDialog.xaml.cs @@ -1,5 +1,5 @@ using System.Windows; -using SynAudio.ViewModels; +using SynAudio.Models; namespace SynAudio.Views { @@ -8,14 +8,15 @@ namespace SynAudio.Views /// public partial class LoginDialog : Window { - public LoginDialogViewModel Result { get; set; } + public Credentials Result { get; } - public LoginDialog(LoginDialogViewModel model) + public LoginDialog(Credentials Credentials) { InitializeComponent(); - this.DataContext = Result = model; - this.Closing += LoginDialog_Closing; - tbPassword.Password = model.Password; + DataContext = Result = Credentials; + Owner = Application.Current.MainWindow; + Closing += LoginDialog_Closing; + tbPassword.Password = Credentials.Password; } private void LoginDialog_Closing(object sender, System.ComponentModel.CancelEventArgs e) diff --git a/SynAudio/Views/MainWindow.xaml b/SynAudio/Views/MainWindow.xaml index bd2846e..2953fea 100644 --- a/SynAudio/Views/MainWindow.xaml +++ b/SynAudio/Views/MainWindow.xaml @@ -163,12 +163,13 @@ - + + diff --git a/SynAudio/Views/SettingsDialog.xaml b/SynAudio/Views/SettingsDialog.xaml index bea14a4..4a8c206 100644 --- a/SynAudio/Views/SettingsDialog.xaml +++ b/SynAudio/Views/SettingsDialog.xaml @@ -3,138 +3,129 @@ 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:app="clr-namespace:SynAudio" + xmlns:app="clr-namespace:SynAudio" xmlns:local="clr-namespace:SynAudio.Views" - xmlns:viewmodels="clr-namespace:SynAudio.ViewModels" mc:Ignorable="d" - d:DataContext="{d:DesignInstance viewmodels:SettingsDialogViewModel}" Title="SettingsDialog" WindowStartupLocation="CenterOwner" Background="LightGray" - Height="500" Width="800" MinHeight="500" MinWidth="800"> - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - - - - - + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - + + diff --git a/SynAudio/Views/SettingsDialog.xaml.cs b/SynAudio/Views/SettingsDialog.xaml.cs index d0abc98..6934832 100644 --- a/SynAudio/Views/SettingsDialog.xaml.cs +++ b/SynAudio/Views/SettingsDialog.xaml.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Windows; using System.Windows.Input; -using SynAudio.Models.Config; using SynAudio.ViewModels; namespace SynAudio.Views @@ -21,9 +20,7 @@ public SettingsDialog(SettingsDialogModel vm) private void btnDisconnect_Click(object sender, RoutedEventArgs e) { - //if (VM.Main.ShowLoginDialog()) - VM.Settings.Connection = new ConnectionSettingsModel(); - Application.Current.Shutdown(); + VM.Main.Disconnect(); } private void btnBackupUserData_Click(object sender, RoutedEventArgs e) diff --git a/Utils/JsonSerialization.cs b/Utils/JsonSerialization.cs index 87c9c11..ec27141 100644 --- a/Utils/JsonSerialization.cs +++ b/Utils/JsonSerialization.cs @@ -55,7 +55,7 @@ public static object DeserializeFromStream(Stream stream) return _serializer.Deserialize(jtr); } } - + public static byte[] SerializeToBytes(object o) { using (var ms = new MemoryStream()) @@ -67,7 +67,6 @@ public static byte[] SerializeToBytes(object o) public static object DeserializeFromBytes(byte[] buffer) => DeserializeFromBytes(buffer, 0, buffer.Length); - public static object DeserializeFromBytes(byte[] buffer, int offset, int count) { using (var ms = new MemoryStream(buffer, offset, count, false)) @@ -75,5 +74,24 @@ public static object DeserializeFromBytes(byte[] buffer, int offset, int count) return DeserializeFromStream(ms); } } + public static T DeserializeFromStream(Stream stream) + { + using (var sr = new StreamReader(stream)) + using (var jtr = new JsonTextReader(sr)) + { + return _serializer.Deserialize(jtr); + } + } + + public static T DeserializeFromBytes(byte[] buffer) + => DeserializeFromBytes(buffer, 0, buffer.Length); + + public static T DeserializeFromBytes(byte[] buffer, int offset, int count) + { + using (var ms = new MemoryStream(buffer, offset, count, false)) + { + return DeserializeFromStream(ms); + } + } } }