From 220fa33bf27d701242a459ed827e814f1b5a9ce7 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Tue, 23 Apr 2024 21:48:51 +0700 Subject: [PATCH 01/18] fix: playlist view early loading crash --- MusicX/ViewModels/PlaylistViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MusicX/ViewModels/PlaylistViewModel.cs b/MusicX/ViewModels/PlaylistViewModel.cs index 5ceb4999..1bc0a8c3 100644 --- a/MusicX/ViewModels/PlaylistViewModel.cs +++ b/MusicX/ViewModels/PlaylistViewModel.cs @@ -56,7 +56,7 @@ public async ValueTask LoadMore() { try { - if (Tracks.Count >= Playlist.Count) + if (Tracks.Count == 0 || Tracks.Count >= Playlist.Count) return; VisibleLoadingMore = Visibility.Visible; var response = await vkService.AudioGetAsync(Playlist.Id, Playlist.OwnerId, Playlist.AccessKey, Tracks.Count, 40); From fa1219b2088021ab593e57e69dd110c3535fcac8 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:00:53 +0700 Subject: [PATCH 02/18] fix: artist blacklist style missing --- MusicX/Views/SettingsView.xaml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/MusicX/Views/SettingsView.xaml b/MusicX/Views/SettingsView.xaml index 9c780b5f..29115330 100644 --- a/MusicX/Views/SettingsView.xaml +++ b/MusicX/Views/SettingsView.xaml @@ -153,14 +153,15 @@ - - + - + + Tag="{Binding}"> + + + + - - + + Date: Wed, 24 Apr 2024 19:36:53 +0700 Subject: [PATCH 03/18] fix: enable mousewheel scroll for queue --- MusicX/Controls/PlayerControl.xaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/MusicX/Controls/PlayerControl.xaml b/MusicX/Controls/PlayerControl.xaml index 6a4e9d50..1cf33e81 100644 --- a/MusicX/Controls/PlayerControl.xaml +++ b/MusicX/Controls/PlayerControl.xaml @@ -629,6 +629,19 @@ + + + + + + + Date: Wed, 24 Apr 2024 21:09:42 +0700 Subject: [PATCH 04/18] restyle titleblock button --- MusicX/App.xaml | 1 + MusicX/Controls/Blocks/TitleBlockControl.xaml | 75 ++++++++---- .../Controls/Blocks/TitleBlockControl.xaml.cs | 48 +++----- MusicX/Styles/HyperlinkButtonStyles.xaml | 109 ++++++++++++++++++ 4 files changed, 174 insertions(+), 59 deletions(-) create mode 100644 MusicX/Styles/HyperlinkButtonStyles.xaml diff --git a/MusicX/App.xaml b/MusicX/App.xaml index 502aef82..1b91eb80 100644 --- a/MusicX/App.xaml +++ b/MusicX/App.xaml @@ -18,6 +18,7 @@ + pack://application:,,,/;component/Fonts/#Segoe Fluent Icons diff --git a/MusicX/Controls/Blocks/TitleBlockControl.xaml b/MusicX/Controls/Blocks/TitleBlockControl.xaml index bf5e812c..288b731d 100644 --- a/MusicX/Controls/Blocks/TitleBlockControl.xaml +++ b/MusicX/Controls/Blocks/TitleBlockControl.xaml @@ -5,9 +5,35 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wpfui="clr-namespace:Wpf.Ui.Controls;assembly=Wpf.Ui" + xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:models="clr-namespace:MusicX.Core.Models;assembly=MusicX.Core" d:DesignHeight="450" d:DesignWidth="800" - mc:Ignorable="d"> + mc:Ignorable="d" + d:DataContext="{d:DesignInstance models:Block}"> + + + + + + + + + + + + + + + + + + - + + + + + - + - - - diff --git a/MusicX/Controls/Blocks/TitleBlockControl.xaml.cs b/MusicX/Controls/Blocks/TitleBlockControl.xaml.cs index c6d39594..83f276c1 100644 --- a/MusicX/Controls/Blocks/TitleBlockControl.xaml.cs +++ b/MusicX/Controls/Blocks/TitleBlockControl.xaml.cs @@ -44,12 +44,14 @@ private void TitleBlockControl_Loaded(object sender, DependencyPropertyChangedEv Title.FontSize = 15; } - Title.Text = block.Layout.Title; + Title.Content = block.Layout.Title; if(block.Layout.TopTitle is not null || block.Layout.Subtitle is not null) { Subtitle.Text = block.Layout.TopTitle?.Text ?? block.Layout.Subtitle; Subtitle.Visibility = Visibility.Visible; + if (block.Actions.Count == 1) + Subtitle.Margin = new(11, 0, 11, 0); } if (block.Badge != null) @@ -58,14 +60,13 @@ private void TitleBlockControl_Loaded(object sender, DependencyPropertyChangedEv BadgeHeader.Visibility = Visibility.Visible; } - if (block.Buttons != null && block.Buttons.Count > 0) //ios + if (block.Buttons is { Count: > 0 }) //ios { if (block.Buttons[0].Options.Count > 0) { ButtonsGrid.Visibility = Visibility.Visible; TitleButtons.Text = block.Buttons[0].Title; Buttons.Visibility = Visibility.Visible; - MoreButton.Visibility = Visibility.Collapsed; foreach (var option in block.Buttons[0].Options) { Buttons.Items.Add(new TextBlock() { Text = option.Text }); @@ -73,47 +74,24 @@ private void TitleBlockControl_Loaded(object sender, DependencyPropertyChangedEv //Buttons.SelectedIndex = 0; return; } - else - { - MoreButton.Visibility = Visibility.Visible; - - MoreButton.Content = block.Buttons[0].Title; - - return; - - } } else { - if(block.Actions.Count > 0) + if(block.Actions.Count > 0 && block.Actions[0].Options.Count > 0) { - - if (block.Actions[0].Options.Count > 0) //android - { - ButtonsGrid.Visibility = Visibility.Visible; - TitleButtons.Text = block.Actions[0].Title; - Buttons.Visibility = Visibility.Visible; - MoreButton.Visibility = Visibility.Collapsed; + //android + ButtonsGrid.Visibility = Visibility.Visible; + TitleButtons.Text = block.Actions[0].Title; + Buttons.Visibility = Visibility.Visible; - foreach (var option in block.Actions[0].Options) - { - Buttons.Items.Add(new TextBlock() { Text = option.Text }); - } - - return; - } - else + foreach (var option in block.Actions[0].Options) { - MoreButton.Visibility = Visibility.Visible; - - MoreButton.Content = block.Actions[0].Title; - - return; - + Buttons.Items.Add(new TextBlock() { Text = option.Text }); } - + + return; } return; diff --git a/MusicX/Styles/HyperlinkButtonStyles.xaml b/MusicX/Styles/HyperlinkButtonStyles.xaml new file mode 100644 index 00000000..c6833f10 --- /dev/null +++ b/MusicX/Styles/HyperlinkButtonStyles.xaml @@ -0,0 +1,109 @@ + + 0,5,0,0 + + + \ No newline at end of file From 04f0f42f2e78e1e05b1f8ae08d0e5d9ca1f6c360 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Thu, 25 Apr 2024 14:40:27 +0700 Subject: [PATCH 05/18] fix: restrict parallel config writes --- MusicX/Services/ConfigService.cs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/MusicX/Services/ConfigService.cs b/MusicX/Services/ConfigService.cs index 8b20fad9..cab82f12 100644 --- a/MusicX/Services/ConfigService.cs +++ b/MusicX/Services/ConfigService.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Microsoft.Win32; using MusicX.Helpers; @@ -12,6 +13,8 @@ namespace MusicX.Services { public class ConfigService { + private static readonly Semaphore ConfigSemaphore = new(1, 1, "MusicX_ConfigSemaphore"); + private readonly JsonSerializerOptions _configSerializerOptions = new(JsonSerializerDefaults.Web) { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower @@ -68,9 +71,17 @@ public async Task GetConfig() ConfigModel? config = null; if(File.Exists(_configPath)) { - await using var stream = File.OpenRead(_configPath); - - config = await JsonSerializer.DeserializeAsync(stream, _configSerializerOptions); + await ConfigSemaphore.WaitOneAsync(); + + try + { + await using var stream = File.OpenRead(_configPath); + config = await JsonSerializer.DeserializeAsync(stream, _configSerializerOptions); + } + finally + { + ConfigSemaphore.Release(); + } } if (config is null) @@ -83,9 +94,18 @@ public async Task GetConfig() public async Task SetConfig(ConfigModel config) { Config = config; + + await ConfigSemaphore.WaitOneAsync(); - await using var stream = File.Create(_configPath); - await JsonSerializer.SerializeAsync(stream, config, _configSerializerOptions); + try + { + await using var stream = File.Create(_configPath); + await JsonSerializer.SerializeAsync(stream, config, _configSerializerOptions); + } + finally + { + ConfigSemaphore.Release(); + } } } } From 2e2e1f44dd55fd643db35d81b75f9db03a0150d3 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:46:56 +0700 Subject: [PATCH 06/18] add check updates button back --- MusicX/RootWindow.xaml.cs | 26 +++---------------- MusicX/Services/UpdateService.cs | 40 +++++++++++++++++++++++++++++ MusicX/Views/SettingsView.xaml | 9 +++++++ MusicX/Views/SettingsView.xaml.cs | 21 +++++---------- MusicX/Views/StartingWindow.xaml.cs | 1 + 5 files changed, 60 insertions(+), 37 deletions(-) create mode 100644 MusicX/Services/UpdateService.cs diff --git a/MusicX/RootWindow.xaml.cs b/MusicX/RootWindow.xaml.cs index 224aeed8..cad5b5ad 100644 --- a/MusicX/RootWindow.xaml.cs +++ b/MusicX/RootWindow.xaml.cs @@ -458,30 +458,10 @@ private async Task CheckUpdatesInStart() try { await Task.Delay(2000); - /*var github = StaticService.Container.GetRequiredService(); - - var release = await github.GetLastRelease(); - - if (release.TagName != StaticService.Version) - navigationService.OpenModal(release);*/ - - var config = await configService.GetConfig(); - - var getBetaUpdates = config.GetBetaUpdates.GetValueOrDefault(false); - var manager = new UpdateManager(new GithubSource("https://github.com/Fooxboy/MusicX-WPF", - string.Empty, getBetaUpdates, new HttpClientFileDownloader()), new() - { - ExplicitChannel = getBetaUpdates ? "win-beta" : "win" - }); - - var updateInfo = await manager.CheckForUpdatesAsync(); - if (updateInfo is null) - return; - - var viewModel = new AvailableNewUpdateModalViewModel(manager, updateInfo); - - navigationService.OpenModal(viewModel); + var updateService = StaticService.Container.GetRequiredService(); + + await updateService.CheckForUpdates(); }catch(Exception ex) { var properties = new Dictionary diff --git a/MusicX/Services/UpdateService.cs b/MusicX/Services/UpdateService.cs new file mode 100644 index 00000000..9b44462c --- /dev/null +++ b/MusicX/Services/UpdateService.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using MusicX.ViewModels.Modals; +using MusicX.Views.Modals; +using Velopack; +using Velopack.Sources; + +namespace MusicX.Services; + +public class UpdateService +{ + private readonly ConfigService _configService; + private readonly NavigationService _navigationService; + + public UpdateService(ConfigService configService, NavigationService navigationService) + { + _configService = configService; + _navigationService = navigationService; + } + + public async Task CheckForUpdates() + { + var getBetaUpdates = _configService.Config.GetBetaUpdates.GetValueOrDefault(); + var manager = new UpdateManager(new GithubSource("https://github.com/Fooxboy/MusicX-WPF", + string.Empty, getBetaUpdates, new HttpClientFileDownloader()), new() + { + ExplicitChannel = getBetaUpdates ? "win-beta" : "win" + }); + + var updateInfo = await manager.CheckForUpdatesAsync(); + + if (updateInfo is null) + return false; + + var viewModel = new AvailableNewUpdateModalViewModel(manager, updateInfo); + + _navigationService.OpenModal(viewModel); + + return true; + } +} \ No newline at end of file diff --git a/MusicX/Views/SettingsView.xaml b/MusicX/Views/SettingsView.xaml index 29115330..16bcc7ca 100644 --- a/MusicX/Views/SettingsView.xaml +++ b/MusicX/Views/SettingsView.xaml @@ -297,6 +297,15 @@ Opacity="0.6" Text="26 марта 2022" /> + + (); +#if !DEBUG try { - var navigation = StaticService.Container.GetRequiredService(); - var github = StaticService.Container.GetRequiredService(); - - var release = await github.GetLastRelease(); - - - - if (release.TagName == StaticService.Version) + var updateService = StaticService.Container.GetRequiredService(); + + if (!await updateService.CheckForUpdates()) { snackbarService.Show("Уже обновлено!", "У Вас установлена последняя версия MusicX! Обновлений пока что нет"); - - } - else - { - navigation.OpenModal(release); } } catch (Exception ex) @@ -237,7 +228,9 @@ private async void CheckUpdates_Click(object sender, RoutedEventArgs e) snackbarService.Show("Ошибка", "Произошла ошибка при проверке обновлений"); } - +#else + snackbarService.Show("В режиме отладки", "Сервис обновлений отключен"); +#endif } private void TelegramButton_Click(object sender, RoutedEventArgs e) diff --git a/MusicX/Views/StartingWindow.xaml.cs b/MusicX/Views/StartingWindow.xaml.cs index 1e022d8e..345e8e50 100644 --- a/MusicX/Views/StartingWindow.xaml.cs +++ b/MusicX/Views/StartingWindow.xaml.cs @@ -112,6 +112,7 @@ await Task.Run(async () => collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); var container = StaticService.Container = collection.BuildServiceProvider(); From 6985b5f93a6f3490ac1719cb52b90fe7421ba3f2 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:57:52 +0700 Subject: [PATCH 07/18] fix: reference counting on winrt decoder --- MusicX/Services/Player/Sources/BoomMediaSource.cs | 4 ++-- MusicX/Services/Player/Sources/MediaSourceBase.cs | 3 --- MusicX/Services/Player/Sources/VkMediaSource.cs | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/MusicX/Services/Player/Sources/BoomMediaSource.cs b/MusicX/Services/Player/Sources/BoomMediaSource.cs index 28018123..e15c3ff9 100644 --- a/MusicX/Services/Player/Sources/BoomMediaSource.cs +++ b/MusicX/Services/Player/Sources/BoomMediaSource.cs @@ -37,9 +37,9 @@ public override async Task OpenWithMediaPlayerAsync(MediaPlayer player, Pl ["headers"] = $"Authorization: {_boomService.Client.DefaultRequestHeaders.Authorization}" }, cancellationToken); - RegisterSourceObjectReference(player, rtMediaSource); - await rtMediaSource.OpenWithMediaPlayerAsync(player).AsTask(cancellationToken); + + RegisterSourceObjectReference(player, rtMediaSource); } catch (Exception e) { diff --git a/MusicX/Services/Player/Sources/MediaSourceBase.cs b/MusicX/Services/Player/Sources/MediaSourceBase.cs index 8f93912c..28689eaf 100644 --- a/MusicX/Services/Player/Sources/MediaSourceBase.cs +++ b/MusicX/Services/Player/Sources/MediaSourceBase.cs @@ -194,9 +194,6 @@ protected static void RegisterSourceObjectReference(MediaPlayer player, IWinRTOb void PlayerOnSourceChanged(MediaPlayer sender, object args) { - if (!ReferenceEquals(player.Source, rtObject)) - return; - player.SourceChanged -= PlayerOnSourceChanged; if (rtObject is IDisposable disposable) diff --git a/MusicX/Services/Player/Sources/VkMediaSource.cs b/MusicX/Services/Player/Sources/VkMediaSource.cs index 6a6b5cc0..d4e9dfb6 100644 --- a/MusicX/Services/Player/Sources/VkMediaSource.cs +++ b/MusicX/Services/Player/Sources/VkMediaSource.cs @@ -26,10 +26,10 @@ public override async Task OpenWithMediaPlayerAsync(MediaPlayer player, Pl try { var rtMediaSource = await CreateWinRtMediaSource(vkData, cancellationToken: cancellationToken); - - RegisterSourceObjectReference(player, rtMediaSource); await rtMediaSource.OpenWithMediaPlayerAsync(player).AsTask(cancellationToken); + + RegisterSourceObjectReference(player, rtMediaSource); } catch (Exception e) { From 9f1ab0c6b89da4e1aaf53dbd700044c9650dba69 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:33:16 +0700 Subject: [PATCH 08/18] save last player last across restarts --- MusicX/Models/ConfigModel.cs | 5 ++ MusicX/RootWindow.xaml.cs | 60 ++++++++++--------- MusicX/Services/Player/PlayerService.cs | 16 ++--- .../AvailableNewUpdateModalViewModel.cs | 9 ++- MusicX/Views/SettingsView.xaml | 13 ++++ MusicX/Views/SettingsView.xaml.cs | 8 +++ MusicX/Views/StartingWindow.xaml.cs | 7 --- 7 files changed, 72 insertions(+), 46 deletions(-) diff --git a/MusicX/Models/ConfigModel.cs b/MusicX/Models/ConfigModel.cs index ed3b180d..5b83a412 100644 --- a/MusicX/Models/ConfigModel.cs +++ b/MusicX/Models/ConfigModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using MusicX.Services.Player; namespace MusicX.Models { @@ -53,5 +54,9 @@ public class ConfigModel public string? DeviceId { get; set; } public string? ExchangeToken { get; set; } + + public bool? SavePlayerState { get; set; } + + public PlayerState? LastPlayerState { get; set; } } } diff --git a/MusicX/RootWindow.xaml.cs b/MusicX/RootWindow.xaml.cs index 224aeed8..c5f9ca6f 100644 --- a/MusicX/RootWindow.xaml.cs +++ b/MusicX/RootWindow.xaml.cs @@ -64,8 +64,6 @@ public RootWindow(NavigationService navigationService, VkService vkService, Logg playerSerivce.TrackChangedEvent += PlayerSerivce_TrackChangedEvent; - Closing += RootWindow_Closing; - SingleAppService.Instance.RunWitchArgs += Instance_RunWitchArgs; togetherService.ConnectedToSession += TogetherServiceOnConnectedToSession; @@ -125,29 +123,6 @@ await Task.Factory.StartNew(async() => } - private async void RootWindow_Closing(object? sender, CancelEventArgs e) - { - try - { - var listenTogetherService = StaticService.Container.GetRequiredService(); - - if (listenTogetherService.IsConnectedToServer && listenTogetherService.PlayerMode != PlayerMode.None) - { - if (listenTogetherService.PlayerMode == PlayerMode.Owner) - { - await listenTogetherService.StopPlaySessionAsync(); - } - else - { - await listenTogetherService.LeavePlaySessionAsync(); - } - } - }catch(Exception ex) - { - //nothing - } - - } private void PlayerSerivce_TrackChangedEvent(object? sender, EventArgs e) { if (PlayerShowed) return; @@ -316,6 +291,13 @@ private async void Window_Loaded(object sender, RoutedEventArgs e) if (config.NotifyMessages is null) config.NotifyMessages = new() { ShowListenTogetherModal = true, LastShowedTelegramBlock = null }; + if (config.LastPlayerState is not null) + { + await playerControl.PlayerService.RestoreFromStateAsync(config.LastPlayerState); + + config.LastPlayerState = null; + } + await configService.SetConfig(config); if(config.NotifyMessages.ShowListenTogetherModal) @@ -575,11 +557,35 @@ private void NotifyIcon_LeftClick(NotifyIcon sender, RoutedEventArgs e) }*/ - private void RootWindow_OnClosing(object? sender, CancelEventArgs e) + private async void RootWindow_OnClosing(object? sender, CancelEventArgs e) { configService.Config.Width = Width; configService.Config.Height = Height; - configService.SetConfig(configService.Config).SafeFireAndForget(continueOnCapturedContext: true); + + if (configService.Config.SavePlayerState is true) + configService.Config.LastPlayerState = PlayerState.CreateOrNull(playerControl.PlayerService); + + await configService.SetConfig(configService.Config); + + try + { + var listenTogetherService = StaticService.Container.GetRequiredService(); + + if (listenTogetherService.IsConnectedToServer && listenTogetherService.PlayerMode != PlayerMode.None) + { + if (listenTogetherService.PlayerMode == PlayerMode.Owner) + { + await listenTogetherService.StopPlaySessionAsync(); + } + else + { + await listenTogetherService.LeavePlaySessionAsync(); + } + } + }catch(Exception ex) + { + //nothing + } } private void RootFrame_Navigating(object sender, System.Windows.Navigation.NavigatingCancelEventArgs e) diff --git a/MusicX/Services/Player/PlayerService.cs b/MusicX/Services/Player/PlayerService.cs index ce52de0a..b2450410 100644 --- a/MusicX/Services/Player/PlayerService.cs +++ b/MusicX/Services/Player/PlayerService.cs @@ -89,8 +89,7 @@ public PlayerService(Logger logger, ISnackbarService snackbarService, public async Task RestoreFromStateAsync(PlayerState state) { - await PlayAsync(state.Playlist, state.Track); - Seek(state.Position); + await PlayAsync(state.Playlist, state.Track, state.Position); } public async void Play() @@ -118,12 +117,13 @@ await Task.WhenAll( _statsListeners.Select(b => b.TrackChangedAsync(previousTrack, CurrentTrack!, ChangeReason.TrackChange))); } - private async Task PlayTrackAsync(PlaylistTrack track) + private async Task PlayTrackAsync(PlaylistTrack track, TimeSpan? position = null) { try { if (CurrentTrack == track) { + if (position is not null) Seek(position.Value); player.Play(); return; } @@ -172,6 +172,8 @@ private async Task PlayTrackAsync(PlaylistTrack track) await NextTrack(); return; } + + if (position is not null) Seek(position.Value); player.Play(); UpdateWindowsData().SafeFireAndForget(); @@ -195,12 +197,12 @@ private async Task PlayTrackAsync(PlaylistTrack track) } - public async Task PlayAsync(IPlaylist playlist, PlaylistTrack? firstTrack = null) + public async Task PlayAsync(IPlaylist playlist, PlaylistTrack? firstTrack = null, TimeSpan? startPosition = null) { if(_listenTogetherService.PlayerMode == PlayerMode.Listener) { await _listenTogetherService.LeavePlaySessionAsync(); - await Application.Current.Dispatcher.InvokeAsync(() => PlayAsync(playlist).SafeFireAndForget()); + await Application.Current.Dispatcher.InvokeAsync(() => PlayAsync(playlist, firstTrack, startPosition).SafeFireAndForget()); return; } @@ -237,7 +239,7 @@ await Application.Current.Dispatcher.InvokeAsync(() => }); } - firstTrackTask = PlayTrackAsync(firstTrack); + firstTrackTask = PlayTrackAsync(firstTrack, startPosition); await Task.WhenAll( _statsListeners.Select(b => b.TrackChangedAsync(CurrentTrack, firstTrack, ChangeReason.PlaylistChange))); @@ -260,7 +262,7 @@ await Task.WhenAll( if (firstTrack is null) { var previousTrack = CurrentTrack; - await PlayTrackAsync(Tracks[0]); + await PlayTrackAsync(Tracks[0], startPosition); await Task.WhenAll( _statsListeners.Select(b => b.TrackChangedAsync(previousTrack, CurrentTrack!, ChangeReason.PlaylistChange))); diff --git a/MusicX/ViewModels/Modals/AvailableNewUpdateModalViewModel.cs b/MusicX/ViewModels/Modals/AvailableNewUpdateModalViewModel.cs index 3740d49c..e695f595 100644 --- a/MusicX/ViewModels/Modals/AvailableNewUpdateModalViewModel.cs +++ b/MusicX/ViewModels/Modals/AvailableNewUpdateModalViewModel.cs @@ -48,12 +48,11 @@ private async Task Execute() await _updateManager.DownloadUpdatesAsync(UpdateInfo, ProgressHandler); var playerState = PlayerState.CreateOrNull(StaticService.Container.GetRequiredService()); + var configService = StaticService.Container.GetRequiredService(); - _updateManager.WaitExitThenApplyUpdates(UpdateInfo, restartArgs: new [] - { - "--play", - playerState is null ? "null" : JsonSerializer.Serialize(playerState) - }); + configService.Config.LastPlayerState = playerState; + + _updateManager.WaitExitThenApplyUpdates(UpdateInfo); Application.Current.Shutdown(); } diff --git a/MusicX/Views/SettingsView.xaml b/MusicX/Views/SettingsView.xaml index 29115330..bea7c027 100644 --- a/MusicX/Views/SettingsView.xaml +++ b/MusicX/Views/SettingsView.xaml @@ -261,6 +261,19 @@ FontSize="18" Unchecked="MinimizeToTray_Unchecked" /> + + + + + diff --git a/MusicX/Views/SettingsView.xaml.cs b/MusicX/Views/SettingsView.xaml.cs index f6fd6574..39ebc129 100644 --- a/MusicX/Views/SettingsView.xaml.cs +++ b/MusicX/Views/SettingsView.xaml.cs @@ -96,6 +96,7 @@ private async void SettingsView_Loaded(object sender, RoutedEventArgs e) WinterTheme.IsChecked = config.WinterTheme.Value; MinimizeToTray.IsChecked = config.MinimizeToTray.Value; GetBetaUpdates.IsChecked = config.GetBetaUpdates.Value; + SavePlayerState.IsChecked = config.SavePlayerState.GetValueOrDefault(); UserName.Text = config.UserName; @@ -586,5 +587,12 @@ private void CatalogsCard_Click(object sender, RoutedEventArgs e) { StaticService.Container.GetRequiredService().OpenSection("profiles"); } + + private async void SavePlayerState_OnCheckChanged(object sender, RoutedEventArgs e) + { + config.SavePlayerState = SavePlayerState.IsChecked; + + await configService.SetConfig(config); + } } } diff --git a/MusicX/Views/StartingWindow.xaml.cs b/MusicX/Views/StartingWindow.xaml.cs index 1e022d8e..a0c5deaa 100644 --- a/MusicX/Views/StartingWindow.xaml.cs +++ b/MusicX/Views/StartingWindow.xaml.cs @@ -186,13 +186,6 @@ await container.GetRequiredService() { await rootWindow.StartListenTogether(arg[1]); } - - var playArgIndex = Array.BinarySearch(_args, "--play", - StringComparer.OrdinalIgnoreCase); - if (playArgIndex >= 0 && playArgIndex + 1 < _args.Length && - JsonSerializer.Deserialize(string.Join(string.Empty, _args[(playArgIndex + 1)..])) is { } state) - await container.GetRequiredService() - .RestoreFromStateAsync(state); } this.Close(); From b01889872a2fdcb9217e19315feec54b8bcb8e9a Mon Sep 17 00:00:00 2001 From: Fooxboy Date: Fri, 26 Apr 2024 15:56:21 +0300 Subject: [PATCH 09/18] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MusicX/Controls/BlockControl.xaml | 2 +- MusicX/Controls/Blocks/TitleBlockControl.xaml | 4 +- MusicX/Controls/PlayerControl.xaml.cs | 4 +- MusicX/Views/SettingsView.xaml | 108 +++++++++++------- MusicX/Views/SettingsView.xaml.cs | 9 ++ 5 files changed, 82 insertions(+), 45 deletions(-) diff --git a/MusicX/Controls/BlockControl.xaml b/MusicX/Controls/BlockControl.xaml index 8abb4d39..06d2abba 100644 --- a/MusicX/Controls/BlockControl.xaml +++ b/MusicX/Controls/BlockControl.xaml @@ -31,7 +31,7 @@ - + diff --git a/MusicX/Controls/Blocks/TitleBlockControl.xaml b/MusicX/Controls/Blocks/TitleBlockControl.xaml index 288b731d..93961d5e 100644 --- a/MusicX/Controls/Blocks/TitleBlockControl.xaml +++ b/MusicX/Controls/Blocks/TitleBlockControl.xaml @@ -24,7 +24,7 @@ Style="{StaticResource HeaderHyperlinkButtonStyle}" FontFamily="{StaticResource VKSansDemiBold}" FontSize="30" - FontWeight="DemiBold" + FontWeight="Bold" Click="MoreButton_Click" Content="{TemplateBinding Content}"> @@ -36,7 +36,7 @@ diff --git a/MusicX/Controls/PlayerControl.xaml.cs b/MusicX/Controls/PlayerControl.xaml.cs index 4f7a860b..7b4b1cb0 100644 --- a/MusicX/Controls/PlayerControl.xaml.cs +++ b/MusicX/Controls/PlayerControl.xaml.cs @@ -286,8 +286,8 @@ private void UpdateSpeakerIcon() { _ when PlayerService.IsMuted => new SymbolIcon(SymbolRegular.SpeakerOff28), 0.0 => new SymbolIcon(SymbolRegular.SpeakerOff28), - > 0.0 and < 0.30 => new SymbolIcon(SymbolRegular.Speaker032), - > 0.30 and < 0.60 => new SymbolIcon(SymbolRegular.Speaker132), + > 0.0 and < 0.10 => new SymbolIcon(SymbolRegular.Speaker032), + > 0.10 and < 0.60 => new SymbolIcon(SymbolRegular.Speaker132), > 0.80 => new SymbolIcon(SymbolRegular.Speaker232), _ => SpeakerIcon.Icon }; diff --git a/MusicX/Views/SettingsView.xaml b/MusicX/Views/SettingsView.xaml index bea7c027..65de18a0 100644 --- a/MusicX/Views/SettingsView.xaml +++ b/MusicX/Views/SettingsView.xaml @@ -83,37 +83,44 @@ FontSize="30" Text="Настройки" /> + - + + + + - + Загрузка треков - + + + + + - - + + - + + + - - + + + + - + Логи - + + + + + - + + + - - + + - + + + - + @@ -331,7 +355,7 @@ Height="30" Margin="10,0,0,0" Appearance="Secondary" - Click="TelegramButton_Click" + Click="TelegramChat_Click" Content="Телеграм чат" /> @@ -366,6 +390,9 @@ NavigateUri="https://github.com/lepoco/wpfui/blob/main/LICENSE" /> + + + @@ -383,6 +410,9 @@ NavigateUri="https://t.me/VkDotNet" /> + + + @@ -396,6 +426,9 @@ NavigateUri="https://github.com/flowersne/VkNet.AudioBypass/blob/master/LICENSE" /> + + + @@ -405,6 +438,8 @@ NavigateUri="https://ffmpeg.org/" /> + + @@ -418,6 +453,8 @@ NavigateUri="https://github.com/cmxl/FFmpeg.NET/blob/master/LICENSE.md" /> + + @@ -431,15 +468,6 @@ NavigateUri="https://github.com/Lachee/discord-rpc-csharp/blob/master/LICENSE" /> - - - - - - diff --git a/MusicX/Views/SettingsView.xaml.cs b/MusicX/Views/SettingsView.xaml.cs index 39ebc129..0d3e7768 100644 --- a/MusicX/Views/SettingsView.xaml.cs +++ b/MusicX/Views/SettingsView.xaml.cs @@ -250,6 +250,15 @@ private void TelegramButton_Click(object sender, RoutedEventArgs e) }); } + private void TelegramChat_Click(object sender, RoutedEventArgs e) + { + Process.Start(new ProcessStartInfo + { + FileName = "https://t.me/+lO37psdwX2s3NjZi", + UseShellExecute = true + }); + } + private void OpenLogs_Click(object sender, RoutedEventArgs e) { Process.Start(new ProcessStartInfo From 792ce1f68ec0b42450886ad702c2a6aba79136b0 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Thu, 25 Apr 2024 15:20:41 +0700 Subject: [PATCH 10/18] implement hide to tray --- MusicX/MusicX.csproj | 2 + MusicX/RootWindow.xaml | 6 +++ MusicX/RootWindow.xaml.cs | 66 +++++++++++++++++++---- MusicX/Views/SettingsView.xaml.cs | 35 ++---------- MusicX/packages.lock.json | 89 ++++++++++++++++++------------- 5 files changed, 119 insertions(+), 79 deletions(-) diff --git a/MusicX/MusicX.csproj b/MusicX/MusicX.csproj index b207a5ac..3d6a016b 100644 --- a/MusicX/MusicX.csproj +++ b/MusicX/MusicX.csproj @@ -86,6 +86,7 @@ + @@ -102,6 +103,7 @@ + diff --git a/MusicX/RootWindow.xaml b/MusicX/RootWindow.xaml index 15f2ae30..7629bacf 100644 --- a/MusicX/RootWindow.xaml +++ b/MusicX/RootWindow.xaml @@ -8,6 +8,7 @@ xmlns:wpfui="clr-namespace:Wpf.Ui.Controls;assembly=Wpf.Ui" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:views="clr-namespace:MusicX.Views" + xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray" d:DesignHeight="720" d:DesignWidth="1280" KeyDown="Window_KeyDown" @@ -135,6 +136,11 @@ + diff --git a/MusicX/RootWindow.xaml.cs b/MusicX/RootWindow.xaml.cs index 4ac8db06..23e1e854 100644 --- a/MusicX/RootWindow.xaml.cs +++ b/MusicX/RootWindow.xaml.cs @@ -11,6 +11,7 @@ using Microsoft.AppCenter.Analytics; using Microsoft.AppCenter.Crashes; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Toolkit.Uwp.Notifications; using MusicX.Controls; using MusicX.Core.Models; using MusicX.Core.Services; @@ -28,6 +29,7 @@ using Wpf.Ui; using Wpf.Ui.Controls; using Wpf.Ui.Extensions; +using Wpf.Ui.Tray.Controls; using NavigationService = MusicX.Services.NavigationService; namespace MusicX @@ -54,6 +56,7 @@ public RootWindow(NavigationService navigationService, VkService vkService, Logg ConfigService configService, ISnackbarService snackbarService, ListenTogetherService togetherService) : base(snackbarService, navigationService, logger) { + Application.Current.MainWindow = this; InitializeComponent(); this.navigationService = navigationService; this.vkService = vkService; @@ -72,6 +75,14 @@ public RootWindow(NavigationService navigationService, VkService vkService, Logg Height = configService.Config.Height; } + protected override void OnStateChanged(EventArgs e) + { + base.OnStateChanged(e); + + if (WindowState == WindowState.Minimized && TrayIcon.IsRegistered && ConsiderHideToTray()) + Hide(); + } + protected override SnackbarPresenter? GetSnackbarPresenter() => RootSnackbar; private async Task TogetherServiceOnConnectedToSession(PlaylistTrack arg) @@ -123,6 +134,44 @@ await Task.Factory.StartNew(async() => } + private bool ConsiderHideToTray() + { + if (configService.Config.MinimizeToTray is not null) return configService.Config.MinimizeToTray.Value; + + new ToastContentBuilder() + .AddText("Приложене было скрыто в трее, нажмите на иконку, чтобы снова открыть окно.") + .AddText("Поведение можно изменить в настройках. Это уведомление больше не будет показано.") + .Show(); + + configService.Config.MinimizeToTray = true; + configService.SetConfig(configService.Config).SafeFireAndForget(); + + return true; + } + + private async void RootWindow_Closing(object? sender, CancelEventArgs e) + { + try + { + var listenTogetherService = StaticService.Container.GetRequiredService(); + + if (listenTogetherService.IsConnectedToServer && listenTogetherService.PlayerMode != PlayerMode.None) + { + if (listenTogetherService.PlayerMode == PlayerMode.Owner) + { + await listenTogetherService.StopPlaySessionAsync(); + } + else + { + await listenTogetherService.LeavePlaySessionAsync(); + } + } + }catch(Exception ex) + { + //nothing + } + + } private void PlayerSerivce_TrackChangedEvent(object? sender, EventArgs e) { if (PlayerShowed) return; @@ -305,18 +354,7 @@ private async void Window_Loaded(object sender, RoutedEventArgs e) navigationService.OpenModal(); } - /*if(config.MinimizeToTray != null) // TODO tray - { - WpfTitleBar.MinimizeToTray = config.MinimizeToTray.Value; - }else - { - WpfTitleBar.MinimizeToTray = false; - }*/ - this.WindowState = WindowState.Normal; - - - // AppNotifyIcon.Register(); } catch (Exception ex) { @@ -574,5 +612,11 @@ private void RootFrame_Navigating(object sender, System.Windows.Navigation.Navig ? Visibility.Visible : Visibility.Collapsed; } + + private void TrayIcon_OnLeftClick(NotifyIcon sender, RoutedEventArgs e) + { + Show(); + WindowState = WindowState.Normal; + } } } diff --git a/MusicX/Views/SettingsView.xaml.cs b/MusicX/Views/SettingsView.xaml.cs index d1f1e19f..326e2f8b 100644 --- a/MusicX/Views/SettingsView.xaml.cs +++ b/MusicX/Views/SettingsView.xaml.cs @@ -65,37 +65,12 @@ private async void SettingsView_Loaded(object sender, RoutedEventArgs e) this.config = await configService.GetConfig(); - if (config.ShowRPC == null) - { - config.ShowRPC = true; - } - - if(config.BroadcastVK == null) - { - config.BroadcastVK = false; - } - - if(config.WinterTheme == null) - { - config.WinterTheme = false; - } - - if (config.MinimizeToTray == null) - { - config.MinimizeToTray = false; - } - - if (config.GetBetaUpdates == null) - { - config.GetBetaUpdates = false; - } - - ShowRPC.IsChecked = config.ShowRPC.Value; - BroacastVK.IsChecked = config.BroadcastVK.Value; + ShowRPC.IsChecked = config.ShowRPC.GetValueOrDefault(); + BroacastVK.IsChecked = config.BroadcastVK.GetValueOrDefault(); ShowAmimatedBackground.IsChecked = config.AnimatedBackground; - WinterTheme.IsChecked = config.WinterTheme.Value; - MinimizeToTray.IsChecked = config.MinimizeToTray.Value; - GetBetaUpdates.IsChecked = config.GetBetaUpdates.Value; + WinterTheme.IsChecked = config.WinterTheme.GetValueOrDefault(); + MinimizeToTray.IsChecked = config.MinimizeToTray.GetValueOrDefault(); + GetBetaUpdates.IsChecked = config.GetBetaUpdates.GetValueOrDefault(); SavePlayerState.IsChecked = config.SavePlayerState.GetValueOrDefault(); UserName.Text = config.UserName; diff --git a/MusicX/packages.lock.json b/MusicX/packages.lock.json index 935ad6a2..7324859d 100644 --- a/MusicX/packages.lock.json +++ b/MusicX/packages.lock.json @@ -55,6 +55,18 @@ "Microsoft.AppCenter": "5.0.3" } }, + "Microsoft.Toolkit.Uwp.Notifications": { + "type": "Direct", + "requested": "[7.1.3, )", + "resolved": "7.1.3", + "contentHash": "A1dglAzb24gjehmb7DwGd07mfyZ1gacAK7ObE0KwDlRc3mayH2QW7cSOy3TkkyELjLg19OQBuhPOj4SpXET9lg==", + "dependencies": { + "Microsoft.Win32.Registry": "4.7.0", + "System.Drawing.Common": "4.7.0", + "System.Reflection.Emit": "4.7.0", + "System.ValueTuple": "4.5.0" + } + }, "Microsoft.VCRTForwarders.140": { "type": "Direct", "requested": "[1.0.8-pre, )", @@ -144,6 +156,16 @@ "resolved": "3.0.4", "contentHash": "Jbt8nJ4MSC/WBhqx6iXOW06Rt2UUNVxA8+23AOSM63jlOIhU6e6P4BIw8rL/UnRPycOdA2vntYl5i7k53E0AGg==" }, + "WPF-UI.Tray": { + "type": "Direct", + "requested": "[3.0.4, )", + "resolved": "3.0.4", + "contentHash": "cYa4eVZqrvRoSXtGgww6yLoSHaGCvSYEfNHDBC/iyv/z8pyWEpwN9EQnJRgGW2AclnG0zrOlhp1aLhoeFkY5qw==", + "dependencies": { + "System.Drawing.Common": "8.0.0", + "WPF-UI": "3.0.4" + } + }, "WpfScreenHelper": { "type": "Direct", "requested": "[2.1.0, )", @@ -359,24 +381,19 @@ "resolved": "8.0.0", "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==" - }, "Microsoft.Win32.Registry": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", + "resolved": "4.7.0", + "contentHash": "KSrRMb5vNi0CWSGG1++id2ZOs/1QhRqROt+qgbEAdQuGjGrFcl4AOl4/exGPUYz2wUnU42nvJqon1T3U0kPXLA==", "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" + "System.Security.AccessControl": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" } }, "Microsoft.Win32.SystemEvents": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" + "resolved": "8.0.0", + "contentHash": "9opKRyOKMCi2xJ7Bj7kxtZ1r9vbzosMvRrdEhVhDz8j8MoBGgB+WmC94yH839NPH+BclAjtQ/pyagvi/8gDLkw==" }, "Microsoft.Windows.CsWinRT": { "type": "Transitive", @@ -488,10 +505,10 @@ }, "System.Drawing.Common": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", + "resolved": "8.0.0", + "contentHash": "JkbHJjtI/dWc5dfmEdJlbe3VwgZqCkZRtfuWFh5GOv0f+gGCfBtzMpIVkmdkj2AObO9y+oiOi81UGwH3aBYuqA==", "dependencies": { - "Microsoft.Win32.SystemEvents": "6.0.0" + "Microsoft.Win32.SystemEvents": "8.0.0" } }, "System.IO.Pipelines": { @@ -512,6 +529,11 @@ "resolved": "4.5.5", "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", "resolved": "6.0.0", @@ -538,11 +560,8 @@ }, "System.Security.Principal.Windows": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "U77HfRXlZlOeIXd//Yoj6Jnk8AXlbeisf1oq1os+hxOGVnuG+lGSfGqTwTZBoORFF6j/0q7HXIl8cqwQ9aUGqQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0" - } + "resolved": "4.7.0", + "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" }, "System.Text.Encodings.Web": { "type": "Transitive", @@ -554,6 +573,11 @@ "resolved": "8.0.0", "contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==" }, + "System.ValueTuple": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==" + }, "System.Windows.Extensions": { "type": "Transitive", "resolved": "6.0.0", @@ -664,31 +688,23 @@ }, "Microsoft.Win32.Registry": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "+FWlwd//+Tt56316p00hVePBCouXyEzT86Jb3+AuRotTND0IYn0OO3obs1gnQEs/txEnt+rF2JBGLItTG+Be6A==", + "resolved": "4.7.0", + "contentHash": "KSrRMb5vNi0CWSGG1++id2ZOs/1QhRqROt+qgbEAdQuGjGrFcl4AOl4/exGPUYz2wUnU42nvJqon1T3U0kPXLA==", "dependencies": { - "System.Security.AccessControl": "4.5.0", - "System.Security.Principal.Windows": "4.5.0" + "System.Security.AccessControl": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" } }, "Microsoft.Win32.SystemEvents": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" + "resolved": "8.0.0", + "contentHash": "9opKRyOKMCi2xJ7Bj7kxtZ1r9vbzosMvRrdEhVhDz8j8MoBGgB+WmC94yH839NPH+BclAjtQ/pyagvi/8gDLkw==" }, "SQLitePCLRaw.lib.e_sqlite3": { "type": "Transitive", "resolved": "2.1.5", "contentHash": "Fqp/FQlb+USnEC2qfWOdsY4fFir3sob9BQMgdT3rcamUAoB7id8V0WknWdsFnE4TXBKDiM79+oPoZoHAuU/dsg==" }, - "System.Drawing.Common": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "NfuoKUiP2nUWwKZN6twGqXioIe1zVD0RIj2t976A+czLHr2nY454RwwXs6JU9Htc6mwqL6Dn/nEL3dpVf2jOhg==", - "dependencies": { - "Microsoft.Win32.SystemEvents": "6.0.0" - } - }, "System.Management": { "type": "Transitive", "resolved": "6.0.0", @@ -709,11 +725,8 @@ }, "System.Security.Principal.Windows": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "U77HfRXlZlOeIXd//Yoj6Jnk8AXlbeisf1oq1os+hxOGVnuG+lGSfGqTwTZBoORFF6j/0q7HXIl8cqwQ9aUGqQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0" - } + "resolved": "4.7.0", + "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" }, "System.Text.Encodings.Web": { "type": "Transitive", From 0839e8610f5208fef8e57b5f76de3942f0201120 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Fri, 26 Apr 2024 20:26:21 +0700 Subject: [PATCH 11/18] remove unused method left from rebase --- MusicX/RootWindow.xaml.cs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/MusicX/RootWindow.xaml.cs b/MusicX/RootWindow.xaml.cs index 23e1e854..6feff5d2 100644 --- a/MusicX/RootWindow.xaml.cs +++ b/MusicX/RootWindow.xaml.cs @@ -149,29 +149,6 @@ private bool ConsiderHideToTray() return true; } - private async void RootWindow_Closing(object? sender, CancelEventArgs e) - { - try - { - var listenTogetherService = StaticService.Container.GetRequiredService(); - - if (listenTogetherService.IsConnectedToServer && listenTogetherService.PlayerMode != PlayerMode.None) - { - if (listenTogetherService.PlayerMode == PlayerMode.Owner) - { - await listenTogetherService.StopPlaySessionAsync(); - } - else - { - await listenTogetherService.LeavePlaySessionAsync(); - } - } - }catch(Exception ex) - { - //nothing - } - - } private void PlayerSerivce_TrackChangedEvent(object? sender, EventArgs e) { if (PlayerShowed) return; From b16550cf861a0befff3e3989cdb3fe0f2870bc37 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Fri, 26 Apr 2024 20:47:01 +0700 Subject: [PATCH 12/18] fix: regression, missing bottom offset on section view --- MusicX/Views/SectionView.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MusicX/Views/SectionView.xaml b/MusicX/Views/SectionView.xaml index 99078624..a20df5f5 100644 --- a/MusicX/Views/SectionView.xaml +++ b/MusicX/Views/SectionView.xaml @@ -123,7 +123,7 @@ IsInertiaEnabled="True" ScrollChanged="SectionScrollViewer_ScrollChanged" hc:ScrollViewerAttach.AutoHide="True"> - + From a0af51e5a3ab93d9df2fd9ac0936334c2451129e Mon Sep 17 00:00:00 2001 From: Fooxboy Date: Fri, 26 Apr 2024 19:31:10 +0300 Subject: [PATCH 13/18] =?UTF-8?q?#373=20=D0=98=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=20?= =?UTF-8?q?=D0=BF=D0=BB=D0=B5=D0=B9=D0=BB=D0=B8=D1=81=D1=82=D0=BE=D0=B2=20?= =?UTF-8?q?"=D0=9A=D0=B0=D0=BA=D0=BE=D0=B9=20=D1=81=D0=B5=D0=B9=D1=87?= =?UTF-8?q?=D0=B0=D1=81=20=D0=B2=D0=B0=D0=B9=D0=B1"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MusicX/Controls/BlockControl.xaml | 6 + MusicX/Controls/CroppedPlaylistControl.xaml | 34 ++++ .../Controls/CroppedPlaylistControl.xaml.cs | 172 ++++++++++++++++++ MusicX/Controls/ListPlaylists.xaml | 12 +- MusicX/Controls/ListPlaylists.xaml.cs | 9 + 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 MusicX/Controls/CroppedPlaylistControl.xaml create mode 100644 MusicX/Controls/CroppedPlaylistControl.xaml.cs diff --git a/MusicX/Controls/BlockControl.xaml b/MusicX/Controls/BlockControl.xaml index 06d2abba..bdf781e2 100644 --- a/MusicX/Controls/BlockControl.xaml +++ b/MusicX/Controls/BlockControl.xaml @@ -12,6 +12,7 @@ xmlns:viewModels="clr-namespace:MusicX.ViewModels.Controls" xmlns:hc="https://handyorg.github.io/handycontrol" d:DesignHeight="450" d:DesignWidth="800" + xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" ContentTemplateSelector="{DynamicResource BlockTemplateSelector}" mc:Ignorable="d"> @@ -68,6 +69,11 @@ + + + + + diff --git a/MusicX/Controls/CroppedPlaylistControl.xaml b/MusicX/Controls/CroppedPlaylistControl.xaml new file mode 100644 index 00000000..8ed71f72 --- /dev/null +++ b/MusicX/Controls/CroppedPlaylistControl.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MusicX/Controls/CroppedPlaylistControl.xaml.cs b/MusicX/Controls/CroppedPlaylistControl.xaml.cs new file mode 100644 index 00000000..633260df --- /dev/null +++ b/MusicX/Controls/CroppedPlaylistControl.xaml.cs @@ -0,0 +1,172 @@ +using Microsoft.AppCenter.Analytics; +using Microsoft.AppCenter.Crashes; +using Microsoft.Extensions.DependencyInjection; +using MusicX.Core.Models; +using MusicX.Core.Services; +using MusicX.Services; +using MusicX.Services.Player.Playlists; +using MusicX.Services.Player; +using MusicX.Views; +using System.Collections.Generic; +using System.ComponentModel; +using System.Windows; +using System.Windows.Controls; +using Wpf.Ui.Controls; +using System; + +namespace MusicX.Controls +{ + /// + /// Логика взаимодействия для CroppedPlaylistControl.xaml + /// + public partial class CroppedPlaylistControl : UserControl + { + + public static readonly DependencyProperty TitleProperty = + DependencyProperty.Register("Title", typeof(string), typeof(CroppedPlaylistControl), new PropertyMetadata(string.Empty)); + + public string Title + { + get { return (string)GetValue(TitleProperty); } + set + { + SetValue(TitleProperty, value); + } + } + + public static readonly DependencyProperty CoverProperty = + DependencyProperty.Register("Cover", typeof(string), typeof(CroppedPlaylistControl), new PropertyMetadata(string.Empty)); + + public string Cover + { + get { return (string)GetValue(CoverProperty); } + set + { + SetValue(CoverProperty, value); + } + } + + public static readonly DependencyProperty PlaylistProperty = + DependencyProperty.Register("Playlist", typeof(Playlist), typeof(CroppedPlaylistControl), new PropertyMetadata(new Playlist())); + + public Playlist Playlist + { + get { return (Playlist)GetValue(PlaylistProperty); } + set + { + SetValue(PlaylistProperty, value); + } + } + + public CroppedPlaylistControl() + { + InitializeComponent(); + } + + private bool _nowPlay = false; + + private void Border_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e) + { + if(sender is Border border) + { + border.Opacity = 1; + } + } + + private void Border_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) + { + if (sender is Border border) + { + border.Opacity = 0.6; + } + } + + private void OpenFullPlaylist(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + var notificationService = StaticService.Container.GetRequiredService(); + + notificationService.OpenExternalPage(new PlaylistView(Playlist)); + } + + private async void PlayPlaylist(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + e.Handled = true; + try + { + var properties = new Dictionary + { +#if DEBUG + { "IsDebug", "True" }, +#endif + {"Version", StaticService.Version } + }; + Analytics.TrackEvent("PlayPlaylistWithButton", properties); + + var playerService = StaticService.Container.GetRequiredService(); + + if (!_nowPlay) + { + _nowPlay = true; + + PlayIcon.Symbol = SymbolRegular.Timer24; + var vkService = StaticService.Container.GetRequiredService(); + + await playerService.PlayAsync( + new VkPlaylistPlaylist(vkService, new(Playlist.Id, Playlist.OwnerId, Playlist.AccessKey))); + + PlayIcon.Symbol = SymbolRegular.Pause24; + + } + else + { + playerService.Pause(); + PlayIcon.Symbol = SymbolRegular.Play24; + + _nowPlay = false; + } + } + catch (Exception ex) + { + + var properties = new Dictionary + { +#if DEBUG + { "IsDebug", "True" }, +#endif + {"Version", StaticService.Version } + }; + Crashes.TrackError(ex, properties); + } + } + + private void UserControl_Loaded(object sender, RoutedEventArgs e) + { + var player = StaticService.Container.GetRequiredService(); + player.CurrentPlaylistChanged += PlayerOnCurrentPlaylistChanged; + } + + private void UserControl_Unloaded(object sender, RoutedEventArgs e) + { + var player = StaticService.Container.GetRequiredService(); + player.CurrentPlaylistChanged -= PlayerOnCurrentPlaylistChanged; + } + + private void PlayerOnCurrentPlaylistChanged(object? sender, EventArgs e) + { + if (sender is not PlayerService service) + return; + + if (service.CurrentPlaylist is VkPlaylistPlaylist { Data: { } data } && data.PlaylistId == Playlist.Id) + { + _nowPlay = true; + PlayIcon.Symbol = SymbolRegular.Pause24; + } + else + { + _nowPlay = false; + PlayIcon.Symbol = SymbolRegular.Play24; + } + } + + } +} diff --git a/MusicX/Controls/ListPlaylists.xaml b/MusicX/Controls/ListPlaylists.xaml index 0feb8f0e..2b62a301 100644 --- a/MusicX/Controls/ListPlaylists.xaml +++ b/MusicX/Controls/ListPlaylists.xaml @@ -22,7 +22,11 @@ - + + + + + + + + + + + diff --git a/MusicX/Controls/ListPlaylists.xaml.cs b/MusicX/Controls/ListPlaylists.xaml.cs index 89271dae..9957a7f9 100644 --- a/MusicX/Controls/ListPlaylists.xaml.cs +++ b/MusicX/Controls/ListPlaylists.xaml.cs @@ -22,5 +22,14 @@ public bool ShowFull get => (bool)GetValue(ShowFullProperty); set => SetValue(ShowFullProperty, value); } + + public static readonly DependencyProperty IsCroppedProperty = DependencyProperty.Register( + nameof(IsCropped), typeof(bool), typeof(ListPlaylists)); + + public bool IsCropped + { + get => (bool)GetValue(IsCroppedProperty); + set => SetValue(IsCroppedProperty, value); + } } } From 0cf30d75d30c76b5da3ed698032c306358000e5e Mon Sep 17 00:00:00 2001 From: Fooxboy Date: Sat, 27 Apr 2024 10:18:20 +0300 Subject: [PATCH 14/18] #365 Audio dislike --- MusicX.Core/Services/VkService.cs | 27 +++++++++++++++ MusicX/Controls/PlayerControl.xaml | 17 ++++++++++ MusicX/Controls/PlayerControl.xaml.cs | 48 +++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/MusicX.Core/Services/VkService.cs b/MusicX.Core/Services/VkService.cs index 66fc81cc..52261962 100644 --- a/MusicX.Core/Services/VkService.cs +++ b/MusicX.Core/Services/VkService.cs @@ -1158,5 +1158,32 @@ public async Task GetLyrics(string audioId) throw; } } + + public async Task Dislike(long audioId, long ownerId) + { + try + { + logger.Info($"Invoke 'audio.addDislike' with audioId {audioId}"); + var parameters = new VkParameters + { + + {"device_id", await _deviceIdStore.GetDeviceIdAsync()}, + + {"audio_ids", $"{ownerId}_{audioId}"}, + }; + + var json = await apiInvoke.InvokeAsync("audio.addDislike", parameters); + logger.Debug("RESULT OF 'audio.addDislike'" + json); + + logger.Info("Successful invoke 'audio.addDislike' "); + + } + catch (Exception ex) + { + logger.Error("VK API ERROR:"); + logger.Error(ex, ex.Message); + throw; + } + } } } diff --git a/MusicX/Controls/PlayerControl.xaml b/MusicX/Controls/PlayerControl.xaml index 1cf33e81..81c02035 100644 --- a/MusicX/Controls/PlayerControl.xaml +++ b/MusicX/Controls/PlayerControl.xaml @@ -330,6 +330,23 @@ + + + + + + + + + (lyricsViewModel); } + + private async void DislikeButton_Click(object sender, RoutedEventArgs e) + { + try + { + var vkService = StaticService.Container.GetRequiredService(); + + if (PlayerService.CurrentTrack?.Data is VkTrackData vkData) + { + await vkService.Dislike(vkData.Info.Id, vkData.Info.OwnerId); + } + + await PlayerService.NextTrack(); + } + catch(Exception ex) + { + var properties = new Dictionary + { + {"Version", StaticService.Version } + }; + Crashes.TrackError(ex, properties); + + logger.Error("Error in dislike track"); + logger.Error(ex, ex.Message); + + var snackbarService = StaticService.Container.GetRequiredService(); + + snackbarService.ShowException("Мы не смогли указать, что Вам этот трек не нравится", ex); + } + } } } From 9126248251db869daa82869e3bb5749c83d79952 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Sat, 27 Apr 2024 22:01:54 +0700 Subject: [PATCH 15/18] add lastfm scrobbling --- .gitmodules | 3 + MusicX.sln | 6 + MusicX/Models/ConfigModel.cs | 5 + MusicX/MusicX.csproj | 1 + .../Player/TrackStats/DiscordTrackStats.cs | 2 +- .../Player/TrackStats/ITrackStatsListener.cs | 2 +- .../Services/Player/TrackStats/LastFmStats.cs | 49 + .../Player/TrackStats/ListenTogetherStats.cs | 2 +- .../TrackStats/VkTrackBroadcastStats.cs | 2 +- .../Player/TrackStats/VkTrackStats.cs | 4 +- .../Modals/LastFmAuthModalViewModel.cs | 64 + MusicX/Views/Modals/LastFmAuthModal.xaml | 35 + MusicX/Views/Modals/LastFmAuthModal.xaml.cs | 19 + MusicX/Views/SettingsView.xaml | 8 + MusicX/Views/SettingsView.xaml.cs | 13 + MusicX/Views/StartingWindow.xaml.cs | 15 + MusicX/packages.lock.json | 1199 +++++++++++++++++ lastfm | 1 + 18 files changed, 1424 insertions(+), 6 deletions(-) create mode 100644 .gitmodules create mode 100644 MusicX/Services/Player/TrackStats/LastFmStats.cs create mode 100644 MusicX/ViewModels/Modals/LastFmAuthModalViewModel.cs create mode 100644 MusicX/Views/Modals/LastFmAuthModal.xaml create mode 100644 MusicX/Views/Modals/LastFmAuthModal.xaml.cs create mode 160000 lastfm diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..cc3083f0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lastfm"] + path = lastfm + url = https://github.com/tolbxela/lastfm.git diff --git a/MusicX.sln b/MusicX.sln index 6a03c4dc..0f9bad8b 100644 --- a/MusicX.sln +++ b/MusicX.sln @@ -28,6 +28,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMediaToolkit", "FFMediaTo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PsCompiler", "PsCompiler\PsCompiler.csproj", "{449B9F89-C7C2-4C7E-9F2F-28CBB731E7F5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IF.Lastfm.Core", "lastfm\src\IF.Lastfm.Core\IF.Lastfm.Core.csproj", "{57464921-52F3-4172-83E9-0B7D75F9DDDF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -74,6 +76,10 @@ Global {449B9F89-C7C2-4C7E-9F2F-28CBB731E7F5}.Debug|x64.Build.0 = Debug|Any CPU {449B9F89-C7C2-4C7E-9F2F-28CBB731E7F5}.Release|x64.ActiveCfg = Release|Any CPU {449B9F89-C7C2-4C7E-9F2F-28CBB731E7F5}.Release|x64.Build.0 = Release|Any CPU + {57464921-52F3-4172-83E9-0B7D75F9DDDF}.Debug|x64.ActiveCfg = Debug|Any CPU + {57464921-52F3-4172-83E9-0B7D75F9DDDF}.Debug|x64.Build.0 = Debug|Any CPU + {57464921-52F3-4172-83E9-0B7D75F9DDDF}.Release|x64.ActiveCfg = Release|Any CPU + {57464921-52F3-4172-83E9-0B7D75F9DDDF}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MusicX/Models/ConfigModel.cs b/MusicX/Models/ConfigModel.cs index 5b83a412..b96b4fb7 100644 --- a/MusicX/Models/ConfigModel.cs +++ b/MusicX/Models/ConfigModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using IF.Lastfm.Core.Objects; using MusicX.Services.Player; namespace MusicX.Models @@ -58,5 +59,9 @@ public class ConfigModel public bool? SavePlayerState { get; set; } public PlayerState? LastPlayerState { get; set; } + + public LastUserSession? LastFmSession { get; set; } + + public bool? SendLastFmScrobbles { get; set; } } } diff --git a/MusicX/MusicX.csproj b/MusicX/MusicX.csproj index 3d6a016b..b1d061c2 100644 --- a/MusicX/MusicX.csproj +++ b/MusicX/MusicX.csproj @@ -109,6 +109,7 @@ + diff --git a/MusicX/Services/Player/TrackStats/DiscordTrackStats.cs b/MusicX/Services/Player/TrackStats/DiscordTrackStats.cs index 0fd4ada3..1d56b07d 100644 --- a/MusicX/Services/Player/TrackStats/DiscordTrackStats.cs +++ b/MusicX/Services/Player/TrackStats/DiscordTrackStats.cs @@ -18,7 +18,7 @@ public DiscordTrackStats(DiscordService discordService, ConfigService configServ _configService = configService; } - public Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason) + public Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason, TimeSpan? position = null) { if (_configService.Config.ShowRPC == true) SetTrack(newTrack); diff --git a/MusicX/Services/Player/TrackStats/ITrackStatsListener.cs b/MusicX/Services/Player/TrackStats/ITrackStatsListener.cs index f66711a0..8a470d06 100644 --- a/MusicX/Services/Player/TrackStats/ITrackStatsListener.cs +++ b/MusicX/Services/Player/TrackStats/ITrackStatsListener.cs @@ -7,6 +7,6 @@ namespace MusicX.Services.Player.TrackStats; public interface ITrackStatsListener { - Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason); + Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason, TimeSpan? position = null); Task TrackPlayStateChangedAsync(PlaylistTrack track, TimeSpan position, bool paused); } \ No newline at end of file diff --git a/MusicX/Services/Player/TrackStats/LastFmStats.cs b/MusicX/Services/Player/TrackStats/LastFmStats.cs new file mode 100644 index 00000000..c127f74a --- /dev/null +++ b/MusicX/Services/Player/TrackStats/LastFmStats.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api; +using IF.Lastfm.Core.Objects; +using IF.Lastfm.Core.Scrobblers; +using MusicX.Models.Enums; +using MusicX.Shared.Player; + +namespace MusicX.Services.Player.TrackStats; + +public class LastFmStats : ITrackStatsListener +{ + private readonly IScrobbler _scrobbler; + private readonly ITrackApi _trackApi; + private readonly ConfigService _configService; + + public LastFmStats(IScrobbler scrobbler, ITrackApi trackApi, ConfigService configService) + { + _scrobbler = scrobbler; + _trackApi = trackApi; + _configService = configService; + } + + public async Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason, TimeSpan? position = null) + { + if (_configService.Config.LastFmSession is null || _configService.Config.SendLastFmScrobbles is not true) + return; + + if (previousTrack is not null && previousTrack.Data.Duration > TimeSpan.FromSeconds(30) && + position.HasValue && (position.Value > TimeSpan.FromMinutes(4) || position.Value > previousTrack.Data.Duration / 2)) + await _scrobbler.ScrobbleAsync(new Scrobble(previousTrack.MainArtists.First().Name, + previousTrack.AlbumId?.Name, previousTrack.Title, DateTimeOffset.Now - position.Value) + { + Duration = previousTrack.Data.Duration + }); + + await _trackApi.UpdateNowPlayingAsync(new Scrobble(newTrack.MainArtists.First().Name, newTrack.AlbumId?.Name, + newTrack.Title, DateTimeOffset.Now) + { + Duration = newTrack.Data.Duration + }); + } + + public Task TrackPlayStateChangedAsync(PlaylistTrack track, TimeSpan position, bool paused) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/MusicX/Services/Player/TrackStats/ListenTogetherStats.cs b/MusicX/Services/Player/TrackStats/ListenTogetherStats.cs index 7caa5ee1..e7955c09 100644 --- a/MusicX/Services/Player/TrackStats/ListenTogetherStats.cs +++ b/MusicX/Services/Player/TrackStats/ListenTogetherStats.cs @@ -15,7 +15,7 @@ public ListenTogetherStats(ListenTogetherService listenTogetherService) this._listenTogetherService = listenTogetherService; } - public async Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason) + public async Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason, TimeSpan? position = null) { if (_listenTogetherService.PlayerMode != PlayerMode.Owner) return; diff --git a/MusicX/Services/Player/TrackStats/VkTrackBroadcastStats.cs b/MusicX/Services/Player/TrackStats/VkTrackBroadcastStats.cs index cacfaf44..a4230066 100644 --- a/MusicX/Services/Player/TrackStats/VkTrackBroadcastStats.cs +++ b/MusicX/Services/Player/TrackStats/VkTrackBroadcastStats.cs @@ -27,7 +27,7 @@ public VkTrackBroadcastStats(VkService vkService, ConfigService configService, L _snackbarService = snackbarService; } - public Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason) + public Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason, TimeSpan? position = null) { try { diff --git a/MusicX/Services/Player/TrackStats/VkTrackStats.cs b/MusicX/Services/Player/TrackStats/VkTrackStats.cs index 531e9310..05bc155e 100644 --- a/MusicX/Services/Player/TrackStats/VkTrackStats.cs +++ b/MusicX/Services/Player/TrackStats/VkTrackStats.cs @@ -17,7 +17,7 @@ public VkTrackStats(VkService vkService) _vkService = vkService; } - public async Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason) + public async Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack newTrack, ChangeReason reason, TimeSpan? position = null) { if (newTrack.Data is not VkTrackData newTrackData) return; @@ -55,7 +55,7 @@ public async Task TrackChangedAsync(PlaylistTrack? previousTrack, PlaylistTrack PlaybackStartedAt = "0", TrackCode = previousTrackData.TrackCode, StreamingType = "online", - Duration = previousTrackData.Duration.TotalSeconds.ToString(), + Duration = (position ?? previousTrackData.Duration).TotalSeconds.ToString(), Repeat = "all", State = "app", Source = newTrackData.ParentBlockId!, diff --git a/MusicX/ViewModels/Modals/LastFmAuthModalViewModel.cs b/MusicX/ViewModels/Modals/LastFmAuthModalViewModel.cs new file mode 100644 index 00000000..3eab33a4 --- /dev/null +++ b/MusicX/ViewModels/Modals/LastFmAuthModalViewModel.cs @@ -0,0 +1,64 @@ +using System.Diagnostics; +using System.Threading.Tasks; +using System.Windows.Input; +using AsyncAwaitBestPractices.MVVM; +using IF.Lastfm.Core.Api; +using IF.Lastfm.Core.Api.Helpers; +using MusicX.Services; +using Wpf.Ui; +using Wpf.Ui.Controls; +using Wpf.Ui.Extensions; +using NavigationService = MusicX.Services.NavigationService; + +namespace MusicX.ViewModels.Modals; + +public class LastFmAuthModalViewModel : BaseViewModel +{ + private readonly ILastAuth _auth; + private readonly NavigationService _navigationService; + private readonly ISnackbarService _snackbarService; + private readonly ConfigService _configService; + private string? _token; + + public ICommand ConfirmCommand { get; } + + public LastFmAuthModalViewModel(ILastAuth auth, NavigationService navigationService, ISnackbarService snackbarService, ConfigService configService) + { + _auth = auth; + _navigationService = navigationService; + _snackbarService = snackbarService; + _configService = configService; + + ConfirmCommand = new AsyncCommand(ConfirmAsync); + } + + private async Task ConfirmAsync() + { + if (_token is null) + return; + + var response = await _auth.GetSessionTokenAsync(_token); + + if (!response.Success) + { + _snackbarService.Show("Ошибка авторизации", $"Сервис вернул: {response.Status}", ControlAppearance.Danger); + return; + } + + _navigationService.CloseModal(); + + _configService.Config.LastFmSession = _auth.UserSession; + await _configService.SetConfig(_configService.Config); + } + + public async Task OpenAuthPageAsync() + { + _token = ((LastResponse)await _auth.GetAuthTokenAsync()).Content; + + Process.Start(new ProcessStartInfo + { + FileName = $"https://last.fm/api/auth/?api_key={_auth.ApiKey}&token={_token}", + UseShellExecute = true + }); + } +} \ No newline at end of file diff --git a/MusicX/Views/Modals/LastFmAuthModal.xaml b/MusicX/Views/Modals/LastFmAuthModal.xaml new file mode 100644 index 00000000..053742b7 --- /dev/null +++ b/MusicX/Views/Modals/LastFmAuthModal.xaml @@ -0,0 +1,35 @@ + + + + + + + + + + Подтвердите вход в аккаунт в браузере + + + + + + + + Готово + + + diff --git a/MusicX/Views/Modals/LastFmAuthModal.xaml.cs b/MusicX/Views/Modals/LastFmAuthModal.xaml.cs new file mode 100644 index 00000000..1244a9f4 --- /dev/null +++ b/MusicX/Views/Modals/LastFmAuthModal.xaml.cs @@ -0,0 +1,19 @@ +using System.Windows; +using System.Windows.Controls; +using MusicX.ViewModels.Modals; + +namespace MusicX.Views.Modals; + +public partial class LastFmAuthModal : Page +{ + public LastFmAuthModal() + { + InitializeComponent(); + } + + private async void LastFmAuthModal_OnLoaded(object sender, RoutedEventArgs e) + { + if (DataContext is LastFmAuthModalViewModel viewModel) + await viewModel.OpenAuthPageAsync(); + } +} \ No newline at end of file diff --git a/MusicX/Views/SettingsView.xaml b/MusicX/Views/SettingsView.xaml index 72d0fdd2..c8946307 100644 --- a/MusicX/Views/SettingsView.xaml +++ b/MusicX/Views/SettingsView.xaml @@ -104,6 +104,14 @@ Content="Показывать трек в статусе ВКонтакте" FontSize="15" Unchecked="BroacastVK_Unchecked" /> + + diff --git a/MusicX/Views/SettingsView.xaml.cs b/MusicX/Views/SettingsView.xaml.cs index 326e2f8b..fff664f9 100644 --- a/MusicX/Views/SettingsView.xaml.cs +++ b/MusicX/Views/SettingsView.xaml.cs @@ -14,6 +14,7 @@ using MusicX.Models; using MusicX.Services; using MusicX.ViewModels; +using MusicX.ViewModels.Modals; using MusicX.Views.Login; using MusicX.Views.Modals; using NLog; @@ -72,6 +73,7 @@ private async void SettingsView_Loaded(object sender, RoutedEventArgs e) MinimizeToTray.IsChecked = config.MinimizeToTray.GetValueOrDefault(); GetBetaUpdates.IsChecked = config.GetBetaUpdates.GetValueOrDefault(); SavePlayerState.IsChecked = config.SavePlayerState.GetValueOrDefault(); + SendLastFm.IsChecked = config.SendLastFmScrobbles.GetValueOrDefault(); UserName.Text = config.UserName; @@ -571,5 +573,16 @@ private async void SavePlayerState_OnCheckChanged(object sender, RoutedEventArgs await configService.SetConfig(config); } + + private async void SendLastFm_OnChanged(object sender, RoutedEventArgs e) + { + config.SendLastFmScrobbles = SendLastFm.IsChecked; + + await configService.SetConfig(config); + + if (config.LastFmSession is null) + StaticService.Container.GetRequiredService() + .OpenModal(StaticService.Container.GetRequiredService()); + } } } diff --git a/MusicX/Views/StartingWindow.xaml.cs b/MusicX/Views/StartingWindow.xaml.cs index 41aa0f8d..1ff74b88 100644 --- a/MusicX/Views/StartingWindow.xaml.cs +++ b/MusicX/Views/StartingWindow.xaml.cs @@ -5,6 +5,8 @@ using System.Text.Json; using System.Threading.Tasks; using System.Windows; +using IF.Lastfm.Core.Api; +using IF.Lastfm.Core.Scrobblers; using Microsoft.AppCenter.Analytics; using Microsoft.Extensions.DependencyInjection; using MusicX.Core.Services; @@ -87,6 +89,7 @@ await Task.Run(async () => collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(); collection.AddTransient(); collection.AddTransient(); @@ -102,6 +105,7 @@ await Task.Run(async () => collection.AddTransient(); collection.AddTransient(); collection.AddTransient(); + collection.AddTransient(); collection.AddSingleton(); collection.AddSingleton(); @@ -113,6 +117,17 @@ await Task.Run(async () => collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); + collection.AddSingleton(s => + { + var auth = new LastAuth("ff3b1adf454799a760ad29a0b71bd6b3", "74ef981214417381716f72a46677a802"); + + if (s.GetRequiredService().Config.LastFmSession is { } session) + auth.LoadSession(session); + + return auth; + }); + collection.AddSingleton(); + collection.AddSingleton(); var container = StaticService.Container = collection.BuildServiceProvider(); diff --git a/MusicX/packages.lock.json b/MusicX/packages.lock.json index 7324859d..0037fdc6 100644 --- a/MusicX/packages.lock.json +++ b/MusicX/packages.lock.json @@ -381,6 +381,16 @@ "resolved": "8.0.0", "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "4.7.0", @@ -459,6 +469,104 @@ "System.Drawing.Common": "6.0.0" } }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, "SQLitePCLRaw.bundle_green": { "type": "Transitive", "resolved": "2.1.5", @@ -494,6 +602,33 @@ "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "6.0.0", @@ -503,6 +638,38 @@ "System.Security.Permissions": "6.0.0" } }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "tD6kosZnTAGdrEa0tZSuFyunMbt/5KYDnHdndJYGqZoNy00XVXyACd5d6KnE1YgYv3ne2CjtAfNXo/fwEhnKUA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.Drawing.Common": { "type": "Transitive", "resolved": "8.0.0", @@ -511,11 +678,92 @@ "Microsoft.Win32.SystemEvents": "8.0.0" } }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "FHNOatmUq0sqJOkTx+UF/9YK1f180cnW5FVqnQMvYUN0elp6wFzbtPSiqbo1/ru8ICp43JM1i7kKkk6GsNGHlA==" }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, "System.Management": { "type": "Transitive", "resolved": "6.0.0", @@ -529,26 +777,301 @@ "resolved": "4.5.5", "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "y7hv0o0weI0j0mvEcBOdt1F3CAADiWlcw3e54m8TfYiRmBPDIsHElx8QUPDlY4x6yWXKPGN0Z2TuXCTPgkm5WQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Reflection.Emit": { "type": "Transitive", "resolved": "4.7.0", "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ==" }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, "System.Security.Cryptography.ProtectedData": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "rp1gMNEZpvx9vP0JW0oHLxlf8oSiQgtno77Y4PLUBjSiDYoD77Y8uXHr1Ea5XG4/pIKhqAdxZ8v8OTUtqo9PeQ==" }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, "System.Security.Permissions": { "type": "Transitive", "resolved": "6.0.0", @@ -563,16 +1086,45 @@ "resolved": "4.7.0", "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, "System.Threading.Channels": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "CMaFr7v+57RW7uZfZkPExsPB6ljwzhjACWW1gfU35Y56rk72B/Wu+sTqxVmGSk4SFUlPc3cjeKND0zktziyjBA==" }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.ValueTuple": { "type": "Transitive", "resolved": "4.5.0", @@ -611,6 +1163,13 @@ "FFmpeg.AutoGen": "[5.1.2.3, )" } }, + "if.lastfm.core": { + "type": "Project", + "dependencies": { + "Newtonsoft.Json": "[9.0.1, )", + "System.Net.Http": "[4.3.2, )" + } + }, "musicx.core": { "type": "Project", "dependencies": { @@ -686,6 +1245,17 @@ "Microsoft.Windows.CsWinRT": "2.0.0" } }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.win.Microsoft.Win32.Primitives": "4.3.0" + } + }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "4.7.0", @@ -700,11 +1270,318 @@ "resolved": "8.0.0", "contentHash": "9opKRyOKMCi2xJ7Bj7kxtZ1r9vbzosMvRrdEhVhDz8j8MoBGgB+WmC94yH839NPH+BclAjtQ/pyagvi/8gDLkw==" }, + "runtime.any.System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "23g6rqftKmovn2cLeGsuHUYm0FD7pdutb0uQMJpZ3qTvq+zHkgmt6J65VtRry4WDGYlmkMa4xDACtaQ94alNag==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "runtime.any.System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1lpifymjGDzoYIaam6/Hyqf8GhBI3xXYLK2TgEvTtuZMorG3Kb9QnMTIKhLjJYXIiu1JvxjngHvtVFQQlpQ3HQ==" + }, + "runtime.any.System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sMDBnad4rp4t7GY442Jux0MCUuKL4otn5BK6Ni0ARTXTSpRNBzZ7hpMfKSvnVSED5kYJm96YOWsqV0JH0d2uuw==" + }, + "runtime.any.System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "M1r+760j1CNA6M/ZaW6KX8gOS8nxPRqloqDcJYVidRG566Ykwcs29AweZs2JF+nMOCgWDiMfPSTMfvwOI9F77w==" + }, + "runtime.any.System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SDZ5AD1DtyRoxYtEcqQ3HDlcrorMYXZeCt7ZhG9US9I5Vva+gpIWDGMkcwa5XiKL0ceQKRZIX2x0XEjLX7PDzQ==" + }, + "runtime.any.System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ==" + }, + "runtime.any.System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Nrm1p3armp6TTf2xuvaa+jGTTmncALWFq22CpmwRvhDf6dE9ZmH40EbOswD4GnFLrMRS0Ki6Kx5aUPmKK/hZBg==" + }, + "runtime.any.System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Lxb89SMvf8w9p9+keBLyL6H6x/TEmc6QVsIIA0T36IuyOY3kNvIdyGddA2qt35cRamzxF8K5p0Opq4G4HjNbhQ==" + }, + "runtime.any.System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fRS7zJgaG9NkifaAxGGclDDoRn9HC7hXACl52Or06a/fxdzDajWb5wov3c6a+gVSlekRoexfjwQSK9sh5um5LQ==", + "dependencies": { + "System.Private.Uri": "4.3.0" + } + }, + "runtime.any.System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GG84X6vufoEzqx8PbeBKheE4srOhimv+yLtGb/JkR3Y2FmoqmueLNFU4Xx8Y67plFpltQSdK74x0qlEhIpv/CQ==" + }, + "runtime.any.System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lBoFeQfxe/4eqjPi46E0LU/YaCMdNkQ8B4MZu/mkzdIAZh8RQ1NYZSj0egrQKdgdvlPFtP4STtob40r4o2DBAw==" + }, + "runtime.any.System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+ihI5VaXFCMVPJNstG4O4eo1CfbrByLxRrQQTqOTp1ttK0kUKDqOdBSTaCB2IBk/QtjDrs6+x4xuezyMXdm0HQ==" + }, + "runtime.any.System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NLrxmLsfRrOuVqPWG+2lrQZnE53MLVeo+w9c54EV+TUo4c8rILpsDXfY8pPiOy9kHpUHHP07ugKmtsU3vVW5Jg==" + }, + "runtime.any.System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w==" + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "runtime.win.Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NU51SEt/ZaD2MF48sJ17BIqx7rjeNNLXUevfMOjqQIetdndXwYjZfZsT6jD+rSWp/FYxjesdK4xUSl4OTEI0jw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "runtime.win.System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "hHHP0WCStene2jjeYcuDkETozUYF/3sHVRHAEOgS3L15hlip24ssqCTnJC28Z03Wpo078oMcJd0H4egD2aJI8g==" + }, + "runtime.win.System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z37zcSCpXuGCYtFbqYO0TwOVXxS2d+BXgSoDFZmRg8BC4Cuy54edjyIvhhcfCrDQA9nl+EPFTgHN54dRAK7mNA==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Overlapped": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "runtime.win.System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lkXXykakvXUU+Zq2j0pC6EO20lEhijjqMc01XXpp1CJN+DeCwl3nsj4t5Xbpz3kA7yQyTqw6d9SyIzsyLsV3zA==", + "dependencies": { + "Microsoft.Win32.Primitives": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "runtime.win.System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RkgHVhUPvzZxuUubiZe8yr/6CypRVXj0VBzaR8hsqQ8f+rUo7e4PWrHTLOCjd8fBMGWCrY//fi7Ku3qXD7oHRw==", + "dependencies": { + "System.Private.Uri": "4.3.0" + } + }, + "runtime.win7.System.Private.Uri": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Q+IBgaPYicSQs2tBlmXqbS25c/JLIthWrgrpMwxKSOobW/OqIMVFruUGfuaz4QABVzV8iKdCAbN7APY7Tclbnw==" + }, "SQLitePCLRaw.lib.e_sqlite3": { "type": "Transitive", "resolved": "2.1.5", "contentHash": "Fqp/FQlb+USnEC2qfWOdsY4fFir3sob9BQMgdT3rcamUAoB7id8V0WknWdsFnE4TXBKDiM79+oPoZoHAuU/dsg==" }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Collections": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.win.System.Diagnostics.Debug": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Diagnostics.Tracing": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Globalization": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Globalization.Calendars": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.any.System.IO": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.win.System.IO.FileSystem": "4.3.0" + } + }, "System.Management": { "type": "Transitive", "resolved": "6.0.0", @@ -713,26 +1590,348 @@ "System.CodeDom": "6.0.0" } }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "y7hv0o0weI0j0mvEcBOdt1F3CAADiWlcw3e54m8TfYiRmBPDIsHElx8QUPDlY4x6yWXKPGN0Z2TuXCTPgkm5WQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "runtime.win.System.Net.Primitives": "4.3.0" + } + }, + "System.Private.Uri": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I4SwANiUGho1esj4V4oSlPllXjzCZDE+5XXso2P03LW2vOda2Enzh8DWOxwN6hnrJyp314c7KuVu31QYhRzOGg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "runtime.win7.System.Private.Uri": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Reflection": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Reflection.Primitives": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Resources.ResourceManager": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "runtime.any.System.Runtime": "4.3.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.win.System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "runtime.any.System.Runtime.InteropServices": "4.3.0" + } + }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ==" }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, "System.Security.Cryptography.ProtectedData": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "rp1gMNEZpvx9vP0JW0oHLxlf8oSiQgtno77Y4PLUBjSiDYoD77Y8uXHr1Ea5XG4/pIKhqAdxZ8v8OTUtqo9PeQ==" }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, "System.Security.Principal.Windows": { "type": "Transitive", "resolved": "4.7.0", "contentHash": "ojD0PX0XhneCsUbAZVKdb7h/70vyYMDYs85lwEI+LngEONe/17A0cFaRFqZU+sOEidcVswYWikYOQ9PPfjlbtQ==" }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Text.Encoding": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.any.System.Text.Encoding.Extensions": "4.3.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Overlapped": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m3HQ2dPiX/DSTpf+yJt8B0c+SRvzfqAJKx+QDWi+VLhz8svLT23MVjEOHPF/KiSLeArKU/iHescrbLd3yVgyNg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Threading.Tasks": "4.3.0" + } + }, "System.Windows.Extensions": { "type": "Transitive", "resolved": "6.0.0", diff --git a/lastfm b/lastfm new file mode 160000 index 00000000..677deaf6 --- /dev/null +++ b/lastfm @@ -0,0 +1 @@ +Subproject commit 677deaf6f4a1fc77aeef691eda068a05d039eb0b From 9e1c77fabce3b338e8b00db155980dd0b651f4e8 Mon Sep 17 00:00:00 2001 From: Fooxboy Date: Sat, 27 Apr 2024 19:29:22 +0300 Subject: [PATCH 16/18] feature: next version [skip ci] --- notes.md | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/notes.md b/notes.md index 48c32419..d1a8c50a 100644 --- a/notes.md +++ b/notes.md @@ -1,14 +1,7 @@ -+ Исправили переключение трека при перемешивании -+ Исправили невозможность отключить перемешивание -+ Починили игнорирование исполнителей -+ Повтор одного трека снова работает -+ Добавили иконки ко всем пунктам контекстного меню трека -+ Исправили наложение иконок explicit lyrics в треках -+ Починили редкий краш загрузки страниц некоторых исполнителей -+ Переделали анимацию загрузки страницы плейлиста -+ Добавили новые разделы рекомендаций на страницу Каталогов -+ Исправили работу кнопок в разделе Музыка -+ Добавили сохранение текущего трека при перезапуске для обновления -+ Обновили версию вконтакте (Добавились плейлисты по настроению) -+ Исправили поведение блока ссылок при переходах назад -+ Провели общую оптимизацию загрузки страниц ++ 💣 Исправлена утечка памяти ++ 🧨 Исправлено сворачивание приложения в трей ++ 🔥 Редизайн заголовка блоков ++ ❤️‍🔥 В настройки добавлена возможность ручной проверки обновлений ++ ⚡ Редизайн плейлистов "Какой сейчас вайб" ++ 🔫 Добавлена возможность сохранения очереди воспроизведения при закрытии приложения ++ ♨️ Реализована возможность дизлайкать треки, которые не нравятся From d55a1afc815e46a6c28d5e9239c228d3f3ab911c Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Sun, 28 Apr 2024 18:36:21 +0700 Subject: [PATCH 17/18] fix: build and lastfm scrobbling --- .gitmodules | 3 - IF.Lastfm.Core/Api/AlbumApi.cs | 102 ++++ IF.Lastfm.Core/Api/ArtistApi.cs | 177 ++++++ IF.Lastfm.Core/Api/ChartApi.cs | 70 +++ .../Api/Commands/Album/AddShoutCommand.cs | 36 ++ .../Api/Commands/Album/GetInfoCommand.cs | 74 +++ .../Api/Commands/Album/GetShoutsCommand.cs | 57 ++ .../Commands/Album/GetTagsByUserCommand.cs | 61 ++ .../Api/Commands/Album/GetTopTagsCommand.cs | 69 +++ .../Api/Commands/Album/SearchCommand.cs | 49 ++ .../Api/Commands/Artist/AddShoutCommand.cs | 31 + .../Api/Commands/Artist/GetInfoCommand.cs | 67 +++ .../Api/Commands/Artist/GetShoutsCommand.cs | 53 ++ .../Api/Commands/Artist/GetSimilarCommand.cs | 67 +++ .../Commands/Artist/GetTagsByUserCommand.cs | 57 ++ .../Commands/Artist/GetTopAlbumsCommand.cs | 54 ++ .../Api/Commands/Artist/GetTopTagsCommand.cs | 71 +++ .../Commands/Artist/GetTopTracksCommand.cs | 50 ++ .../Api/Commands/Artist/SearchCommand.cs | 49 ++ .../Commands/Auth/GetMobileSessionCommand.cs | 54 ++ .../Api/Commands/Auth/GetSessionCommand.cs | 49 ++ .../Api/Commands/Auth/GetTokenCommand.cs | 42 ++ .../Commands/Chart/GetTopArtistsCommand.cs | 41 ++ .../Api/Commands/Chart/GetTopTagsCommand.cs | 43 ++ .../Api/Commands/Chart/GetTopTracksCommand.cs | 42 ++ .../Api/Commands/GetAsyncCommandBase.cs | 53 ++ IF.Lastfm.Core/Api/Commands/IAsyncCommand.cs | 10 + .../Api/Commands/LastAsyncCommandBase.cs | 93 +++ .../Api/Commands/Library/GetArtistsCommand.cs | 49 ++ .../Api/Commands/Library/GetTracksCommand.cs | 63 +++ .../Commands/Library/RemoveScrobbleCommand.cs | 31 + .../Commands/Library/RemoveTrackCommand.cs | 27 + .../Api/Commands/PostAsyncCommandBase.cs | 65 +++ .../Api/Commands/Tag/GetInfoCommand.cs | 47 ++ .../Api/Commands/Tag/GetSimilarCommand.cs | 47 ++ .../Api/Commands/Tag/GetTopAlbumsCommand.cs | 47 ++ .../Api/Commands/Tag/GetTopArtistsCommand.cs | 53 ++ .../Api/Commands/Tag/GetTopTagsCommand.cs | 40 ++ .../Api/Commands/Tag/GetTopTracksCommand.cs | 47 ++ .../Api/Commands/Track/AddShoutCommand.cs | 36 ++ .../Api/Commands/Track/GetInfoCommand.cs | 65 +++ .../Api/Commands/Track/GetShoutsCommand.cs | 57 ++ .../Api/Commands/Track/GetSimilarCommand.cs | 63 +++ .../Api/Commands/Track/LoveCommand.cs | 32 ++ .../Api/Commands/Track/ScrobbleCommand.cs | 68 +++ .../Api/Commands/Track/SearchCommand.cs | 54 ++ .../Api/Commands/Track/UnloveCommand.cs | 32 ++ .../Commands/Track/UpdateNowPlayingCommand.cs | 58 ++ .../UnauthenticatedPostAsyncCommandBase.cs | 17 + .../Api/Commands/User/AddShoutCommand.cs | 32 ++ .../Api/Commands/User/GetInfoCommand.cs | 47 ++ .../Commands/User/GetLovedTracksCommand.cs | 52 ++ .../Commands/User/GetRecentStationsCommand.cs | 55 ++ .../Commands/User/GetRecentTracksCommand.cs | 70 +++ .../User/GetRecommendedArtistsCommand.cs | 40 ++ .../Api/Commands/User/GetShoutsCommand.cs | 49 ++ .../Api/Commands/User/GetTopAlbumsCommand.cs | 51 ++ .../Api/Commands/User/GetTopArtistsCommand.cs | 51 ++ .../User/GetWeeklyAlbumChartCommand.cs | 56 ++ .../User/GetWeeklyArtistChartCommand.cs | 56 ++ .../User/GetWeeklyChartListCommand.cs | 46 ++ .../User/GetWeeklyTrackChartCommand.cs | 56 ++ .../Api/Enums/LastResponseStatus.cs | 99 ++++ IF.Lastfm.Core/Api/Enums/LastStatsTimeSpan.cs | 39 ++ IF.Lastfm.Core/Api/Helpers/ApiExtensions.cs | 54 ++ .../Api/Helpers/ApiNameAttribute.cs | 25 + IF.Lastfm.Core/Api/Helpers/LastResponse.cs | 62 ++ IF.Lastfm.Core/Api/Helpers/LastResponse{T}.cs | 20 + IF.Lastfm.Core/Api/Helpers/PageResponse.cs | 227 ++++++++ IF.Lastfm.Core/Api/IAlbumApi.cs | 41 ++ IF.Lastfm.Core/Api/IArtistApi.cs | 57 ++ IF.Lastfm.Core/Api/IChartApi.cs | 19 + IF.Lastfm.Core/Api/ILastAuth.cs | 55 ++ IF.Lastfm.Core/Api/ILibraryAPI.cs | 35 ++ IF.Lastfm.Core/Api/ITagApi.cs | 16 + IF.Lastfm.Core/Api/ITrackApi.cs | 39 ++ IF.Lastfm.Core/Api/IUserApi.cs | 53 ++ IF.Lastfm.Core/Api/LastAuth.cs | 117 ++++ IF.Lastfm.Core/Api/LastfmClient.cs | 59 ++ IF.Lastfm.Core/Api/LibraryApi.cs | 54 ++ IF.Lastfm.Core/Api/TagApi.cs | 103 ++++ IF.Lastfm.Core/Api/TrackApi.cs | 118 ++++ IF.Lastfm.Core/Api/UserApi.cs | 184 ++++++ IF.Lastfm.Core/Helpers/ApiBase.cs | 41 ++ .../Helpers/CountingHttpClientHandler.cs | 17 + .../Helpers/EnumerableExtensions.cs | 29 + IF.Lastfm.Core/Helpers/FakeResponseHandler.cs | 36 ++ .../Helpers/QueueFakeResponseHandler.cs | 36 ++ IF.Lastfm.Core/IF.Lastfm.Core.csproj | 18 + IF.Lastfm.Core/Json/LastFmBooleanConverter.cs | 25 + .../Json/PageResponseJsonConverter.cs | 39 ++ IF.Lastfm.Core/LastFm.cs | 126 +++++ IF.Lastfm.Core/MD5.cs | 295 ++++++++++ IF.Lastfm.Core/Objects/BuyLink.cs | 6 + IF.Lastfm.Core/Objects/CountryCode.cs | 6 + IF.Lastfm.Core/Objects/ILastFmObject.cs | 7 + IF.Lastfm.Core/Objects/LastAlbum.cs | 142 +++++ IF.Lastfm.Core/Objects/LastArtist.cs | 156 +++++ IF.Lastfm.Core/Objects/LastImageSet.cs | 91 +++ IF.Lastfm.Core/Objects/LastShout.cs | 46 ++ IF.Lastfm.Core/Objects/LastStation.cs | 21 + IF.Lastfm.Core/Objects/LastStats.cs | 25 + IF.Lastfm.Core/Objects/LastTag.cs | 77 +++ IF.Lastfm.Core/Objects/LastTrack.cs | 192 +++++++ IF.Lastfm.Core/Objects/LastUser.cs | 74 +++ IF.Lastfm.Core/Objects/LastUserSession.cs | 17 + IF.Lastfm.Core/Objects/LastWeeklyChartList.cs | 38 ++ IF.Lastfm.Core/Objects/LastWiki.cs | 52 ++ IF.Lastfm.Core/Objects/Scrobble.cs | 93 +++ IF.Lastfm.Core/Scrobblers/IScrobbler.cs | 17 + IF.Lastfm.Core/Scrobblers/MemoryScrobbler.cs | 49 ++ IF.Lastfm.Core/Scrobblers/ScrobbleResponse.cs | 63 +++ IF.Lastfm.Core/Scrobblers/ScrobblerBase.cs | 114 ++++ IF.Lastfm.Core/packages.lock.json | 420 ++++++++++++++ MusicX.sln | 2 +- MusicX/MusicX.csproj | 2 +- MusicX/Services/Player/PlayerService.cs | 16 +- MusicX/packages.lock.json | 533 +++++++++++++++++- lastfm | 1 - 119 files changed, 7661 insertions(+), 20 deletions(-) delete mode 100644 .gitmodules create mode 100644 IF.Lastfm.Core/Api/AlbumApi.cs create mode 100644 IF.Lastfm.Core/Api/ArtistApi.cs create mode 100644 IF.Lastfm.Core/Api/ChartApi.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Album/AddShoutCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Album/GetInfoCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Album/GetShoutsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Album/GetTagsByUserCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Album/GetTopTagsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Album/SearchCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/AddShoutCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/GetInfoCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/GetShoutsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/GetSimilarCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/GetTagsByUserCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/GetTopAlbumsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/GetTopTagsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/GetTopTracksCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Artist/SearchCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Auth/GetMobileSessionCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Auth/GetSessionCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Auth/GetTokenCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Chart/GetTopArtistsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Chart/GetTopTagsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Chart/GetTopTracksCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/GetAsyncCommandBase.cs create mode 100644 IF.Lastfm.Core/Api/Commands/IAsyncCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/LastAsyncCommandBase.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Library/GetArtistsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Library/GetTracksCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Library/RemoveScrobbleCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Library/RemoveTrackCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/PostAsyncCommandBase.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Tag/GetInfoCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Tag/GetSimilarCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Tag/GetTopAlbumsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Tag/GetTopArtistsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Tag/GetTopTagsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Tag/GetTopTracksCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/AddShoutCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/GetInfoCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/GetShoutsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/GetSimilarCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/LoveCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/ScrobbleCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/SearchCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/UnloveCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/Track/UpdateNowPlayingCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/UnauthenticatedPostAsyncCommandBase.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/AddShoutCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetInfoCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetLovedTracksCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetRecentStationsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetRecentTracksCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetRecommendedArtistsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetShoutsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetTopAlbumsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetTopArtistsCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetWeeklyAlbumChartCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetWeeklyArtistChartCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetWeeklyChartListCommand.cs create mode 100644 IF.Lastfm.Core/Api/Commands/User/GetWeeklyTrackChartCommand.cs create mode 100644 IF.Lastfm.Core/Api/Enums/LastResponseStatus.cs create mode 100644 IF.Lastfm.Core/Api/Enums/LastStatsTimeSpan.cs create mode 100644 IF.Lastfm.Core/Api/Helpers/ApiExtensions.cs create mode 100644 IF.Lastfm.Core/Api/Helpers/ApiNameAttribute.cs create mode 100644 IF.Lastfm.Core/Api/Helpers/LastResponse.cs create mode 100644 IF.Lastfm.Core/Api/Helpers/LastResponse{T}.cs create mode 100644 IF.Lastfm.Core/Api/Helpers/PageResponse.cs create mode 100644 IF.Lastfm.Core/Api/IAlbumApi.cs create mode 100644 IF.Lastfm.Core/Api/IArtistApi.cs create mode 100644 IF.Lastfm.Core/Api/IChartApi.cs create mode 100644 IF.Lastfm.Core/Api/ILastAuth.cs create mode 100644 IF.Lastfm.Core/Api/ILibraryAPI.cs create mode 100644 IF.Lastfm.Core/Api/ITagApi.cs create mode 100644 IF.Lastfm.Core/Api/ITrackApi.cs create mode 100644 IF.Lastfm.Core/Api/IUserApi.cs create mode 100644 IF.Lastfm.Core/Api/LastAuth.cs create mode 100644 IF.Lastfm.Core/Api/LastfmClient.cs create mode 100644 IF.Lastfm.Core/Api/LibraryApi.cs create mode 100644 IF.Lastfm.Core/Api/TagApi.cs create mode 100644 IF.Lastfm.Core/Api/TrackApi.cs create mode 100644 IF.Lastfm.Core/Api/UserApi.cs create mode 100644 IF.Lastfm.Core/Helpers/ApiBase.cs create mode 100644 IF.Lastfm.Core/Helpers/CountingHttpClientHandler.cs create mode 100644 IF.Lastfm.Core/Helpers/EnumerableExtensions.cs create mode 100644 IF.Lastfm.Core/Helpers/FakeResponseHandler.cs create mode 100644 IF.Lastfm.Core/Helpers/QueueFakeResponseHandler.cs create mode 100644 IF.Lastfm.Core/IF.Lastfm.Core.csproj create mode 100644 IF.Lastfm.Core/Json/LastFmBooleanConverter.cs create mode 100644 IF.Lastfm.Core/Json/PageResponseJsonConverter.cs create mode 100644 IF.Lastfm.Core/LastFm.cs create mode 100644 IF.Lastfm.Core/MD5.cs create mode 100644 IF.Lastfm.Core/Objects/BuyLink.cs create mode 100644 IF.Lastfm.Core/Objects/CountryCode.cs create mode 100644 IF.Lastfm.Core/Objects/ILastFmObject.cs create mode 100644 IF.Lastfm.Core/Objects/LastAlbum.cs create mode 100644 IF.Lastfm.Core/Objects/LastArtist.cs create mode 100644 IF.Lastfm.Core/Objects/LastImageSet.cs create mode 100644 IF.Lastfm.Core/Objects/LastShout.cs create mode 100644 IF.Lastfm.Core/Objects/LastStation.cs create mode 100644 IF.Lastfm.Core/Objects/LastStats.cs create mode 100644 IF.Lastfm.Core/Objects/LastTag.cs create mode 100644 IF.Lastfm.Core/Objects/LastTrack.cs create mode 100644 IF.Lastfm.Core/Objects/LastUser.cs create mode 100644 IF.Lastfm.Core/Objects/LastUserSession.cs create mode 100644 IF.Lastfm.Core/Objects/LastWeeklyChartList.cs create mode 100644 IF.Lastfm.Core/Objects/LastWiki.cs create mode 100644 IF.Lastfm.Core/Objects/Scrobble.cs create mode 100644 IF.Lastfm.Core/Scrobblers/IScrobbler.cs create mode 100644 IF.Lastfm.Core/Scrobblers/MemoryScrobbler.cs create mode 100644 IF.Lastfm.Core/Scrobblers/ScrobbleResponse.cs create mode 100644 IF.Lastfm.Core/Scrobblers/ScrobblerBase.cs create mode 100644 IF.Lastfm.Core/packages.lock.json delete mode 160000 lastfm diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index cc3083f0..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "lastfm"] - path = lastfm - url = https://github.com/tolbxela/lastfm.git diff --git a/IF.Lastfm.Core/Api/AlbumApi.cs b/IF.Lastfm.Core/Api/AlbumApi.cs new file mode 100644 index 00000000..3ae6402a --- /dev/null +++ b/IF.Lastfm.Core/Api/AlbumApi.cs @@ -0,0 +1,102 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Commands.Album; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public class AlbumApi : ApiBase, IAlbumApi + { + public AlbumApi(ILastAuth auth, HttpClient httpClient = null) + : base(httpClient) + { + Auth = auth; + } + + public async Task> GetInfoAsync(string artistname, string albumname, bool autocorrect = false, string username = null) + { + var command = new GetInfoCommand(Auth, albumname, artistname) + { + Autocorrect = autocorrect, + HttpClient = HttpClient, + UserName = username + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetInfoByMbidAsync(string albumMbid, bool autocorrect = false, string username = null) + { + var command = new GetInfoCommand(Auth) + { + AlbumMbid = albumMbid, + Autocorrect = autocorrect, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + //public Task> GetBuyLinksForAlbumAsync(string artist, string album, CountryCode country, bool autocorrect = false) + //{ + // throw new NotImplementedException(); + //} + + public Task> GetTagsByUserAsync(string artist, string album, string username, bool autocorrect = false) + { + var command = new GetTagsByUserCommand(Auth, artist, album, username) + { + Autocorrect = autocorrect, + HttpClient = HttpClient + }; + + return command.ExecuteAsync(); + } + + public async Task> GetTopTagsAsync(string artist, string album, bool autocorrect = false) + { + var command = new GetTopTagsCommand(Auth) + { + ArtistName = artist, + AlbumName = album, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> SearchAsync(string albumname, int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new SearchCommand(Auth, albumname) + { + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetShoutsAsync(string albumname, string artistname, bool autocorrect = false, int page = 1, int count = LastFm.DefaultPageLength) + { + var command = new GetShoutsCommand(Auth, albumname, artistname) + { + Page = page, + Autocorrect = autocorrect, + Count = count, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + //public async Task AddShoutAsync(string albumname, string artistname, string message) + //{ + // var command = new AddShoutCommand(Auth, albumname, artistname, message); + + // return await command.ExecuteAsync(); + //} + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/ArtistApi.cs b/IF.Lastfm.Core/Api/ArtistApi.cs new file mode 100644 index 00000000..bdb9a457 --- /dev/null +++ b/IF.Lastfm.Core/Api/ArtistApi.cs @@ -0,0 +1,177 @@ +using System.Net.Http; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Commands.Artist; +using IF.Lastfm.Core.Helpers; + +namespace IF.Lastfm.Core.Api +{ + public class ArtistApi : ApiBase, IArtistApi + { + public ArtistApi(ILastAuth auth, HttpClient httpClient = null) + : base(httpClient) + { + Auth = auth; + } + + + + public async Task> GetInfoAsync(string artist, string bioLang = LastFm.DefaultLanguageCode, bool autocorrect = false) + { + var command = new GetInfoCommand(Auth) + { + ArtistName = artist, + BioLanguage = bioLang, + Autocorrect = autocorrect, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetInfoByMbidAsync(string mbid, string bioLang = LastFm.DefaultLanguageCode, bool autocorrect = false) + { + var command = new GetInfoCommand(Auth) + { + ArtistMbid = mbid, + BioLanguage = bioLang, + Autocorrect = autocorrect, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetTopAlbumsAsync(string artist, bool autocorrect = false, int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTopAlbumsCommand(Auth) + { + ArtistName = artist, + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetTopAlbumsByMbidAsync(string mbid, bool autocorrect = false, int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTopAlbumsCommand(Auth) + { + ArtistMbid = mbid, + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetTopTracksAsync(string artist, bool autocorrect = false, int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTopTracksCommand(Auth, artist) + { + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetSimilarAsync(string artistname, bool autocorrect = false, int limit = LastFm.DefaultPageLength) + { + var command = new GetSimilarCommand(Auth) + { + ArtistName = artistname, + Autocorrect = autocorrect, + Limit = limit, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetSimilarByMbidAsync(string mbid, bool autocorrect = false, int limit = LastFm.DefaultPageLength) + { + var command = new GetSimilarCommand(Auth) + { + ArtistMbid = mbid, + Autocorrect = autocorrect, + Limit = limit, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public Task> GetTagsByUserAsync(string artist, string username, bool autocorrect = false, int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTagsByUserCommand(Auth, artist, username) + { + Autocorrect = autocorrect, + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + + return command.ExecuteAsync(); + } + + public Task> GetTopTagsAsync(string artist, bool autocorrect = false) + { + var command = new GetTopTagsCommand(Auth) + { + ArtistName = artist, + Autocorrect = autocorrect, + HttpClient = HttpClient + }; + + return command.ExecuteAsync(); + } + + public Task> GetTopTagsByMbidAsync(string mbid, bool autocorrect = false) + { + var command = new GetTopTagsCommand(Auth) + { + ArtistMbid = mbid, + Autocorrect = autocorrect, + HttpClient = HttpClient + }; + + return command.ExecuteAsync(); + } + + + public async Task> GetShoutsAsync(string artist, int page = 0, int count = LastFm.DefaultPageLength, bool autocorrect = false) + { + var command = new GetShoutsCommand(Auth, artist) + { + Autocorrect = autocorrect, + Page = page, + Count = count, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task AddShoutAsync(string artistname, string message) + { + var command = new AddShoutCommand(Auth, artistname, message) + { + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> SearchAsync(string artistname, int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new SearchCommand(Auth, artistname) + { + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/ChartApi.cs b/IF.Lastfm.Core/Api/ChartApi.cs new file mode 100644 index 00000000..579b7c77 --- /dev/null +++ b/IF.Lastfm.Core/Api/ChartApi.cs @@ -0,0 +1,70 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Commands.Chart; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public class ChartApi : ApiBase, IChartApi + { + + public ChartApi(ILastAuth auth, HttpClient httpClient = null) + : base(httpClient) + { + Auth = auth; + } + + /// + /// Get a list of the most-scrobbled artists on Last.fm. + /// + /// + /// Bug 28/05/16 - itemsPerPage parameter doesn't seem to work all the time; certain values cause more or fewer items to be returned + /// + public Task> GetTopArtistsAsync(int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTopArtistsCommand(Auth) + { + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + return command.ExecuteAsync(); + } + + /// + /// Get a list of the most-scrobbled tracks on Last.fm. + /// + /// + /// Bug 28/05/16 - itemsPerPage parameter doesn't seem to work all the time; certain values cause more or fewer items to be returned + /// + public Task> GetTopTracksAsync(int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTopTracksCommand(Auth) + { + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + return command.ExecuteAsync(); + } + + /// + /// Get a list of the most frequently used tags by Last.fm users + /// + /// + /// Bug 28/05/16 - page and itemsPerPage parameters do not actually affect the number of or selection of tags returned + /// + public Task> GetTopTagsAsync(int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTopTagsCommand(Auth) + { + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + return command.ExecuteAsync(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Album/AddShoutCommand.cs b/IF.Lastfm.Core/Api/Commands/Album/AddShoutCommand.cs new file mode 100644 index 00000000..0a9518cc --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Album/AddShoutCommand.cs @@ -0,0 +1,36 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands.Album +{ + [ApiMethodName("album.shout")] + internal class AddShoutCommand : PostAsyncCommandBase + { + public string Album { get; set; } + + public string Artist { get; set; } + + public string Message { get; set; } + + public AddShoutCommand(ILastAuth auth, string album, string artist, string message) + : base(auth) + { + Album = album; + Artist = artist; + Message = message; + } + + public override void SetParameters() + { + Parameters.Add("album", Album); + Parameters.Add("artist", Artist); + Parameters.Add("message", Message); + } + + public override async Task HandleResponse(HttpResponseMessage response) + { + return await LastResponse.HandleResponse(response); + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Album/GetInfoCommand.cs b/IF.Lastfm.Core/Api/Commands/Album/GetInfoCommand.cs new file mode 100644 index 00000000..ca552045 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Album/GetInfoCommand.cs @@ -0,0 +1,74 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Album +{ + [ApiMethodName("album.getInfo")] + internal class GetInfoCommand : GetAsyncCommandBase> + { + public string AlbumMbid { get; set; } + + public string ArtistName { get; set; } + + public string AlbumName { get; set; } + + public string UserName { get; set; } + + public bool Autocorrect { get; set; } + + public GetInfoCommand(ILastAuth auth) : base(auth) { } + + public GetInfoCommand(ILastAuth auth, string album, string artist) + : this(auth) + { + AlbumName = album; + ArtistName = artist; + } + + public override void SetParameters() + { + if (AlbumMbid != null) + { + Parameters.Add("mbid", AlbumMbid); + } + else + { + Parameters.Add("artist", ArtistName); + Parameters.Add("album", AlbumName); + } + + if (UserName != null) + { + Parameters.Add("username", UserName); + } + + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var album = LastAlbum.ParseJToken(jtoken.SelectToken("album")); + + return LastResponse.CreateSuccessResponse(album); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Album/GetShoutsCommand.cs b/IF.Lastfm.Core/Api/Commands/Album/GetShoutsCommand.cs new file mode 100644 index 00000000..a143074a --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Album/GetShoutsCommand.cs @@ -0,0 +1,57 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Album +{ + [ApiMethodName("album.getShouts")] + internal class GetShoutsCommand : GetAsyncCommandBase> + { + public string AlbumName { get; set; } + + public string ArtistName { get; set; } + + public bool Autocorrect { get; set; } + + public GetShoutsCommand(ILastAuth auth, string albumname, string artistname) + : base(auth) + { + AlbumName = albumname; + ArtistName = artistname; + } + + public override void SetParameters() + { + Parameters.Add("album", AlbumName); + Parameters.Add("artist", ArtistName); + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json).SelectToken("shouts"); + var itemsToken = jtoken.SelectToken("shout"); + var pageInfoToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastShout.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Album/GetTagsByUserCommand.cs b/IF.Lastfm.Core/Api/Commands/Album/GetTagsByUserCommand.cs new file mode 100644 index 00000000..9760eaf7 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Album/GetTagsByUserCommand.cs @@ -0,0 +1,61 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Album +{ + [ApiMethodName("album.getTags")] + internal class GetTagsByUserCommand : GetAsyncCommandBase> + { + public string ArtistName { get; set; } + + public string AlbumName { get; set; } + + public string Username { get; set; } + + public bool Autocorrect { get; set; } + + public GetTagsByUserCommand(ILastAuth auth, string artist, string album, string username) + : base(auth) + { + ArtistName = artist; + AlbumName = album; + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("artist", ArtistName); + Parameters.Add("album", AlbumName); + Parameters.Add("user", Username); + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("tags"); + var itemsToken = resultsToken.SelectToken("tag"); + + return PageResponse.CreateSuccessResponse(itemsToken, token => LastTag.ParseJToken(token)); + } + else + { + return PageResponse.CreateErrorResponse(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Album/GetTopTagsCommand.cs b/IF.Lastfm.Core/Api/Commands/Album/GetTopTagsCommand.cs new file mode 100644 index 00000000..e7fd0370 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Album/GetTopTagsCommand.cs @@ -0,0 +1,69 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Album +{ + [ApiMethodName("album.getTopTags")] + internal class GetTopTagsCommand : GetAsyncCommandBase> + { + public string AlbumMbid { get; set; } + + public string ArtistName { get; set; } + + public string AlbumName { get; set; } + + public bool Autocorrect { get; set; } + + public GetTopTagsCommand(ILastAuth auth) : base(auth) { } + + public GetTopTagsCommand(ILastAuth auth, string album, string artist) + : this(auth) + { + AlbumName = album; + ArtistName = artist; + } + + public override void SetParameters() + { + if (AlbumMbid != null) + { + Parameters.Add("mbid", AlbumMbid); + } + else + { + Parameters.Add("artist", ArtistName); + Parameters.Add("album", AlbumName); + } + + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("toptags"); + var itemsToken = resultsToken.SelectToken("tag"); + + return PageResponse.CreateSuccessResponse(itemsToken, resultsToken, token => LastTag.ParseJToken(token), LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Album/SearchCommand.cs b/IF.Lastfm.Core/Api/Commands/Album/SearchCommand.cs new file mode 100644 index 00000000..1060b798 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Album/SearchCommand.cs @@ -0,0 +1,49 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Album +{ + [ApiMethodName("album.search")] + internal class SearchCommand : GetAsyncCommandBase> + { + public string AlbumName { get; set; } + + public SearchCommand(ILastAuth auth, string albumName) + : base(auth) + { + AlbumName = albumName; + } + + public override void SetParameters() + { + Parameters.Add("album", AlbumName); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("results"); + var itemsToken = resultsToken.SelectToken("albummatches").SelectToken("album"); + + return PageResponse.CreateSuccessResponse(itemsToken, resultsToken, LastAlbum.ParseJToken, LastPageResultsType.OpenQuery); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Artist/AddShoutCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/AddShoutCommand.cs new file mode 100644 index 00000000..bdc27223 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/AddShoutCommand.cs @@ -0,0 +1,31 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.shout")] + internal class AddShoutCommand : PostAsyncCommandBase + { + public string Artist { get; set; } + + public string Message { get; set; } + + public AddShoutCommand(ILastAuth auth, string artist, string message) : base(auth) + { + Artist = artist; + Message = message; + } + + public override void SetParameters() + { + Parameters.Add("artist", Artist); + Parameters.Add("message", Message); + } + + public override async Task HandleResponse(HttpResponseMessage response) + { + return await LastResponse.HandleResponse(response); + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Artist/GetInfoCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/GetInfoCommand.cs new file mode 100644 index 00000000..9b9b8ab5 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/GetInfoCommand.cs @@ -0,0 +1,67 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.getInfo")] + internal class GetInfoCommand : GetAsyncCommandBase> + { + public string ArtistMbid { get; set; } + + public string ArtistName { get; set; } + + public string BioLanguage { get; set; } + + public bool Autocorrect { get; set; } + + public GetInfoCommand(ILastAuth auth) : base(auth) { } + + /// + /// TODO Bio language + /// + public override void SetParameters() + { + if (ArtistMbid != null) + { + Parameters.Add("mbid", ArtistMbid); + } + else + { + Parameters.Add("artist", ArtistName); + } + + if (BioLanguage != null) + { + Parameters.Add("lang", BioLanguage); + } + + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var artist = LastArtist.ParseJToken(jtoken.SelectToken("artist")); + + return LastResponse.CreateSuccessResponse(artist); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Artist/GetShoutsCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/GetShoutsCommand.cs new file mode 100644 index 00000000..709220dd --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/GetShoutsCommand.cs @@ -0,0 +1,53 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.getShouts")] + internal class GetShoutsCommand : GetAsyncCommandBase> + { + public string ArtistName { get; set; } + public bool Autocorrect { get; set; } + + public GetShoutsCommand(ILastAuth auth, string artistname) + : base(auth) + { + ArtistName = artistname; + } + + public override void SetParameters() + { + Parameters.Add("artist", ArtistName); + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var shoutsToken = jtoken.SelectToken("shouts"); + var itemsToken = shoutsToken.SelectToken("shout"); + var pageInfoToken = shoutsToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastShout.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Artist/GetSimilarCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/GetSimilarCommand.cs new file mode 100644 index 00000000..e9f9b023 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/GetSimilarCommand.cs @@ -0,0 +1,67 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.getSimilar")] + internal class GetSimilarCommand : GetAsyncCommandBase> + { + public bool Autocorrect { get; set; } + + public string ArtistMbid { get; set; } + + public string ArtistName { get; set; } + + public int? Limit { get; set; } + + public GetSimilarCommand(ILastAuth auth) + : base(auth){} + + + public override void SetParameters() + { + + if (ArtistMbid != null) + { + Parameters.Add("mbid", ArtistMbid); + } + else + { + Parameters.Add("artist", ArtistName); + } + + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + if (Limit != null) + { + Parameters.Add("limit", Limit.ToString()); + } + + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var itemsToken = jtoken.SelectToken("similarartists").SelectToken("artist"); + + return PageResponse.CreateSuccessResponse(itemsToken, LastArtist.ParseJToken); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Artist/GetTagsByUserCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/GetTagsByUserCommand.cs new file mode 100644 index 00000000..c447f74a --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/GetTagsByUserCommand.cs @@ -0,0 +1,57 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.getTags")] + internal class GetTagsByUserCommand : GetAsyncCommandBase> + { + public string ArtistName { get; set; } + + public string Username { get; set; } + + public bool Autocorrect { get; set; } + + public GetTagsByUserCommand(ILastAuth auth, string artist, string username) + : base(auth) + { + ArtistName = artist; + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("artist", ArtistName); + Parameters.Add("user", Username); + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("tags"); + var itemsToken = resultsToken.SelectToken("tag"); + + return PageResponse.CreateSuccessResponse(itemsToken, token => LastTag.ParseJToken(token)); + } + else + { + return PageResponse.CreateErrorResponse(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Artist/GetTopAlbumsCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/GetTopAlbumsCommand.cs new file mode 100644 index 00000000..8e0f6e12 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/GetTopAlbumsCommand.cs @@ -0,0 +1,54 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.getTopAlbums")] + internal class GetTopAlbumsCommand : GetAsyncCommandBase> + { + public string ArtistName { get; set; } + public string ArtistMbid { get; set; } + + public GetTopAlbumsCommand(ILastAuth auth ) + : base(auth) { } + + public override void SetParameters() + { + if (ArtistMbid != null) + { + Parameters.Add("mbid", ArtistMbid); + } + else + { + Parameters.Add("artist", ArtistName); + } + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var albumsToken = jtoken.SelectToken("topalbums"); + var itemsToken = albumsToken.SelectToken("album"); + var pageInfoToken = albumsToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastAlbum.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Artist/GetTopTagsCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/GetTopTagsCommand.cs new file mode 100644 index 00000000..52f1f468 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/GetTopTagsCommand.cs @@ -0,0 +1,71 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.getTopTags")] + internal class GetTopTagsCommand : GetAsyncCommandBase> + { + public string ArtistMbid { get; set; } + + public string ArtistName { get; set; } + + public bool Autocorrect { get; set; } + + public GetTopTagsCommand(ILastAuth auth) : base(auth) + { + } + + public override void SetParameters() + { + var hasMbid = !string.IsNullOrEmpty(ArtistMbid); + var hasName = !string.IsNullOrEmpty(ArtistName); + + if (!hasMbid && !hasName) + { + throw new InvalidOperationException($"Either {nameof(ArtistMbid)} or {nameof(ArtistName)} must be set"); + } + + if (hasMbid && hasName) + { + throw new InvalidOperationException($""); + } + + if (hasMbid) + { + Parameters.Add("mbid", ArtistMbid); + } + else + { + Parameters.Add("artist", ArtistName); + } + + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("toptags"); + var itemsToken = resultsToken.SelectToken("tag"); + + return PageResponse.CreateSuccessResponse(itemsToken, LastTag.ParseJToken); + } + else + { + return PageResponse.CreateErrorResponse(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Artist/GetTopTracksCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/GetTopTracksCommand.cs new file mode 100644 index 00000000..84e29550 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/GetTopTracksCommand.cs @@ -0,0 +1,50 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.getTopTracks")] + internal class GetTopTracksCommand : GetAsyncCommandBase> + { + public string ArtistName { get; set; } + + public GetTopTracksCommand(ILastAuth auth, string artistname) + : base(auth) + { + ArtistName = artistname; + } + + public override void SetParameters() + { + Parameters.Add("artist", ArtistName); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var tracksToken = jtoken.SelectToken("toptracks"); + var itemsToken = tracksToken.SelectToken("track"); + var pageInfoToken = tracksToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastTrack.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Artist/SearchCommand.cs b/IF.Lastfm.Core/Api/Commands/Artist/SearchCommand.cs new file mode 100644 index 00000000..e6f936b9 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Artist/SearchCommand.cs @@ -0,0 +1,49 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Artist +{ + [ApiMethodName("artist.search")] + internal class SearchCommand : GetAsyncCommandBase> + { + public string ArtistName { get; set; } + + public SearchCommand(ILastAuth auth, string artistName) + : base(auth) + { + ArtistName = artistName; + } + + public override void SetParameters() + { + Parameters.Add("artist", ArtistName); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("results"); + var itemsToken = resultsToken.SelectToken("artistmatches").SelectToken("artist"); + + return PageResponse.CreateSuccessResponse(itemsToken, resultsToken, LastArtist.ParseJToken, LastPageResultsType.OpenQuery); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Auth/GetMobileSessionCommand.cs b/IF.Lastfm.Core/Api/Commands/Auth/GetMobileSessionCommand.cs new file mode 100644 index 00000000..86431caf --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Auth/GetMobileSessionCommand.cs @@ -0,0 +1,54 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Auth +{ + [ApiMethodName("auth.getMobileSession")] + internal class GetMobileSessionCommand : UnauthenticatedPostAsyncCommandBase> + { + public string Username { get; set; } + + public string Password { get; set; } + + public GetMobileSessionCommand(ILastAuth auth, string username, string password) : base(auth) + { + Username = username; + Password = password; + } + + protected override Uri BuildRequestUrl() + { + return new Uri(LastFm.ApiRootSsl, UriKind.Absolute); + } + + public override void SetParameters() + { + Parameters.Add("username", Username); + Parameters.Add("password", Password); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var sessionObject = JsonConvert.DeserializeObject(json).GetValue("session"); + var session = JsonConvert.DeserializeObject(sessionObject.ToString()); + + return LastResponse.CreateSuccessResponse(session); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Auth/GetSessionCommand.cs b/IF.Lastfm.Core/Api/Commands/Auth/GetSessionCommand.cs new file mode 100644 index 00000000..081fb58c --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Auth/GetSessionCommand.cs @@ -0,0 +1,49 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Auth +{ + [ApiMethodName("auth.getSession")] + internal class GetSessionCommand : UnauthenticatedPostAsyncCommandBase> + { + private string Token { get; } + + public GetSessionCommand(ILastAuth auth, string authToken) : base(auth) + { + Token = authToken; + } + + protected override Uri BuildRequestUrl() + { + return new Uri(LastFm.ApiRootSsl, UriKind.Absolute); + } + + public override void SetParameters() + { + Parameters.Add("token", Token); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + if (LastFm.IsResponseValid(json, out LastResponseStatus status) && response.IsSuccessStatusCode) + { + var sessionObject = JsonConvert.DeserializeObject(json).GetValue("session"); + var session = JsonConvert.DeserializeObject(sessionObject.ToString()); + + return LastResponse.CreateSuccessResponse(session); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Auth/GetTokenCommand.cs b/IF.Lastfm.Core/Api/Commands/Auth/GetTokenCommand.cs new file mode 100644 index 00000000..b0ebf06d --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Auth/GetTokenCommand.cs @@ -0,0 +1,42 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Auth +{ + [ApiMethodName("auth.getToken")] + internal class GetTokenCommand : UnauthenticatedPostAsyncCommandBase> + { + public GetTokenCommand(ILastAuth auth) : base(auth) + { + } + + protected override Uri BuildRequestUrl() + { + return new Uri(LastFm.ApiRootSsl, UriKind.Absolute); + } + + public override void SetParameters() + { + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + if (LastFm.IsResponseValid(json, out LastResponseStatus status) && response.IsSuccessStatusCode) + { + var token = JsonConvert.DeserializeObject(json).GetValue("token"); + return LastResponse.CreateSuccessResponse(token.Value()); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Chart/GetTopArtistsCommand.cs b/IF.Lastfm.Core/Api/Commands/Chart/GetTopArtistsCommand.cs new file mode 100644 index 00000000..d3b6c07f --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Chart/GetTopArtistsCommand.cs @@ -0,0 +1,41 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Chart +{ + [ApiMethodName("chart.getTopArtists")] + internal class GetTopArtistsCommand : GetAsyncCommandBase> + { + public GetTopArtistsCommand(ILastAuth auth) : base(auth) { } + + public override void SetParameters() + { + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json).SelectToken("artists"); + var itemsToken = jtoken.SelectToken("artist"); + var pageInfoToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastArtist.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Chart/GetTopTagsCommand.cs b/IF.Lastfm.Core/Api/Commands/Chart/GetTopTagsCommand.cs new file mode 100644 index 00000000..2aa38c8f --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Chart/GetTopTagsCommand.cs @@ -0,0 +1,43 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Chart +{ + [ApiMethodName("chart.getTopTags")] + internal class GetTopTagsCommand : GetAsyncCommandBase> + { + public GetTopTagsCommand(ILastAuth auth) : base(auth) + { + } + + public override void SetParameters() + { + // 28/05/16 Paging parameters don't actually seem to do anything + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jo = JObject.Parse(json); + var tagsToken = jo.SelectToken("tags.tag"); + var pageInfoToken = jo.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(tagsToken, pageInfoToken, LastTag.ParseJToken, LastPageResultsType.Attr); + } + else + { + return PageResponse.CreateErrorResponse(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Chart/GetTopTracksCommand.cs b/IF.Lastfm.Core/Api/Commands/Chart/GetTopTracksCommand.cs new file mode 100644 index 00000000..dbd30cc1 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Chart/GetTopTracksCommand.cs @@ -0,0 +1,42 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Chart +{ + [ApiMethodName("chart.getTopTracks")] + internal class GetTopTracksCommand : GetAsyncCommandBase> + { + public GetTopTracksCommand(ILastAuth auth) : base(auth) { } + + public override void SetParameters() + { + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var tracksToken = jtoken.SelectToken("tracks"); + var itemsToken = tracksToken.SelectToken("track"); + var pageInfoToken = tracksToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastTrack.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/GetAsyncCommandBase.cs b/IF.Lastfm.Core/Api/Commands/GetAsyncCommandBase.cs new file mode 100644 index 00000000..04f7d43f --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/GetAsyncCommandBase.cs @@ -0,0 +1,53 @@ +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace IF.Lastfm.Core.Api.Commands +{ + public abstract class GetAsyncCommandBase : LastAsyncCommandBase where T : LastResponse, new() + { + protected GetAsyncCommandBase(ILastAuth auth) + { + Auth = auth; + } + + public override async Task ExecuteAsync() + { + SetParameters(); + + EscapeParameters(); + + Url = BuildRequestUrl(); + + try + { + var httpClient = HttpClient; + using (var response = await httpClient.GetAsync(Url)) + { + return await HandleResponse(response); + } + } + catch (HttpRequestException) + { + return LastResponse.CreateErrorResponse(LastResponseStatus.RequestFailed); + } + } + + protected override Uri BuildRequestUrl() + { + var apiUrl = LastFm.FormatApiUrl(Method, Auth.ApiKey, Parameters); + return new Uri(apiUrl, UriKind.Absolute); + } + + private void EscapeParameters() + { + foreach (var key in Parameters.Keys.ToList()) + { + Parameters[key] = Uri.EscapeDataString(Parameters[key]); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/IAsyncCommand.cs b/IF.Lastfm.Core/Api/Commands/IAsyncCommand.cs new file mode 100644 index 00000000..b5260bd8 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/IAsyncCommand.cs @@ -0,0 +1,10 @@ +using IF.Lastfm.Core.Api.Helpers; +using System.Threading.Tasks; + +namespace IF.Lastfm.Core.Api.Commands +{ + public interface IAsyncCommand where T : LastResponse, new() + { + Task ExecuteAsync(); + } +} diff --git a/IF.Lastfm.Core/Api/Commands/LastAsyncCommandBase.cs b/IF.Lastfm.Core/Api/Commands/LastAsyncCommandBase.cs new file mode 100644 index 00000000..2067fbe2 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/LastAsyncCommandBase.cs @@ -0,0 +1,93 @@ +using IF.Lastfm.Core.Api.Helpers; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using System.Reflection; + +namespace IF.Lastfm.Core.Api.Commands +{ + /// + /// Having this type makes reflection easier - there probably isn't any other need for it + /// + public abstract class LastAsyncCommandBase + { + private string _methodName; + + public string Method + { + get + { + if (!String.IsNullOrEmpty(_methodName)) + { + return _methodName; + } + + var methodNameAttribute = this.GetType().GetTypeInfo().GetCustomAttribute(); + if (methodNameAttribute == null) + { + throw new NotImplementedException(@"Could not find an ApiMethodNameAttribute on the current Command implementation. +This custom attribute must be present on all Commands. For more information, see the ApiMethodNameAttribute documentation."); + } + return methodNameAttribute.Text; + } + internal set { _methodName = value; } + } + + public Dictionary Parameters { get; set; } + + /// + /// The HttpClient used for the request. + /// + public HttpClient HttpClient { get; set; } + } + + public abstract class LastAsyncCommandBase : LastAsyncCommandBase, IAsyncCommand where T : LastResponse, new() + { + public ILastAuth Auth { get; protected set; } + + private int _page; + + public int Page + { + get => _page == 0 ? LastFm.DefaultPage : _page; + set + { + if (value < 1) throw new ArgumentOutOfRangeException(nameof(value), "Page property cannot be less than 1"); + _page = value; + } + } + + public int Count { get; set; } + + public Uri Url { get; protected set; } + + protected LastAsyncCommandBase() + { + Parameters = new Dictionary(); + } + + public abstract void SetParameters(); + + protected abstract Uri BuildRequestUrl(); + + protected void AddPagingParameters() + { + Parameters.Add("page", Page.ToString()); + Parameters.Add("limit", Count.ToString()); + } + + /// + /// Annoying workaround for Windows Phone's caching... + /// see http://stackoverflow.com/questions/6334788/windows-phone-7-webrequest-caching + /// + protected void DisableCaching() + { + Parameters.Add("disablecachetoken", DateTime.UtcNow.Ticks.ToString()); + } + + public abstract Task ExecuteAsync(); + + public abstract Task HandleResponse(HttpResponseMessage response); + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Library/GetArtistsCommand.cs b/IF.Lastfm.Core/Api/Commands/Library/GetArtistsCommand.cs new file mode 100644 index 00000000..0d0ce1ad --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Library/GetArtistsCommand.cs @@ -0,0 +1,49 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Library +{ + [ApiMethodName("library.getArtists")] + internal class GetArtistsCommand : GetAsyncCommandBase> + { + public string Username { get; } + + public GetArtistsCommand(ILastAuth auth, string username) : base(auth) + { + Username = username; + Page = 1; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json).SelectToken("artists"); + var tracksToken = jtoken.SelectToken("artist"); + var pageInfoToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(tracksToken, pageInfoToken, LastArtist.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Library/GetTracksCommand.cs b/IF.Lastfm.Core/Api/Commands/Library/GetTracksCommand.cs new file mode 100644 index 00000000..5cf85c31 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Library/GetTracksCommand.cs @@ -0,0 +1,63 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Library +{ + [ApiMethodName("library.getTracks")] + internal class GetTracksCommand : GetAsyncCommandBase> + { + public string Username { get; } + + public string Artist { get; } + + public string Album { get; } + + public DateTimeOffset Since { get; } + + public GetTracksCommand(ILastAuth auth, string username, string artist, string album, DateTimeOffset since) + : base(auth) + { + Username = username; + Artist = artist; + Album = album; + Since = since; + Page = 1; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + Parameters.Add("artist", Artist); + Parameters.Add("album", Album); + Parameters.Add("from", Since.AsUnixTime().ToString()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json).SelectToken("tracks"); + var tracksToken = jtoken.SelectToken("track"); + var pageInfoToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(tracksToken, pageInfoToken, LastTrack.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Library/RemoveScrobbleCommand.cs b/IF.Lastfm.Core/Api/Commands/Library/RemoveScrobbleCommand.cs new file mode 100644 index 00000000..7a0ca530 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Library/RemoveScrobbleCommand.cs @@ -0,0 +1,31 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands.Library { + [ApiMethodName("library.removeScrobble")] + internal class RemoveScrobbleCommand : PostAsyncCommandBase { + public string Artist { get; set; } + + public string Track { get; set; } + public DateTimeOffset Timestamp { get; set; } + + public RemoveScrobbleCommand( ILastAuth auth, string artist, string track, DateTimeOffset timestamp ) : base( auth ) { + Artist = artist; + Track = track; + Timestamp = timestamp; + } + + + public override void SetParameters() { + Parameters.Add( "artist", Artist ); + Parameters.Add( "track", Track ); + Parameters.Add( "timestamp", Timestamp.AsUnixTime().ToString() ); + } + + public override async Task HandleResponse( HttpResponseMessage response ) { + return await LastResponse.HandleResponse( response ); + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Library/RemoveTrackCommand.cs b/IF.Lastfm.Core/Api/Commands/Library/RemoveTrackCommand.cs new file mode 100644 index 00000000..4633aaef --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Library/RemoveTrackCommand.cs @@ -0,0 +1,27 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands.Library { + [ApiMethodName("library.removeTrack")] + internal class RemoveTrackCommand : PostAsyncCommandBase { + public string Artist { get; set; } + + public string Track { get; set; } + + public RemoveTrackCommand( ILastAuth auth, string artist, string track) : base( auth ) { + Artist = artist; + Track = track; + } + + + public override void SetParameters() { + Parameters.Add( "artist", Artist ); + Parameters.Add( "track", Track ); + } + + public override async Task HandleResponse( HttpResponseMessage response ) { + return await LastResponse.HandleResponse( response ); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/PostAsyncCommandBase.cs b/IF.Lastfm.Core/Api/Commands/PostAsyncCommandBase.cs new file mode 100644 index 00000000..a3183eea --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/PostAsyncCommandBase.cs @@ -0,0 +1,65 @@ +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using System; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +namespace IF.Lastfm.Core.Api.Commands +{ + public abstract class PostAsyncCommandBase : LastAsyncCommandBase where T : LastResponse, new() + { + protected PostAsyncCommandBase(ILastAuth auth) + { + Auth = auth; + } + + protected override Uri BuildRequestUrl() + { + return new Uri(LastFm.ApiRoot, UriKind.Absolute); + } + + public override Task ExecuteAsync() + { + if (!Auth.Authenticated) + { + return Task.FromResult(LastResponse.CreateErrorResponse(LastResponseStatus.BadAuth)); + } + + return ExecuteAsyncInternal(); + } + + protected async Task ExecuteAsyncInternal() + { + SetParameters(); + + var toRemove = Parameters.Where(p => String.IsNullOrEmpty(p.Value)).ToList(); + foreach (var parameter in toRemove) + { + Parameters.Remove(parameter.Key); + } + + Url = BuildRequestUrl(); + + var apisig = Auth.GenerateMethodSignature(Method, Parameters); + + var postContent = LastFm.CreatePostBody(Method, + Auth.ApiKey, + apisig, + Parameters); + + try + { + var httpClient = HttpClient; + using (var response = await httpClient.PostAsync(Url, postContent)) + { + return await HandleResponse(response); + } + } + catch (HttpRequestException) + { + return LastResponse.CreateErrorResponse(LastResponseStatus.RequestFailed); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Tag/GetInfoCommand.cs b/IF.Lastfm.Core/Api/Commands/Tag/GetInfoCommand.cs new file mode 100644 index 00000000..72ba67d1 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Tag/GetInfoCommand.cs @@ -0,0 +1,47 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Tag +{ + [ApiMethodName("tag.getInfo")] + public class GetInfoCommand:GetAsyncCommandBase> + { + public string TagName { get; set; } + + public GetInfoCommand(ILastAuth auth, string tagName) + : base(auth) + { + TagName = tagName; + } + + public override void SetParameters() + { + Parameters.Add("tag", TagName); + + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var tag = LastTag.ParseJToken(jtoken.SelectToken("tag")); + + return LastResponse.CreateSuccessResponse(tag); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Tag/GetSimilarCommand.cs b/IF.Lastfm.Core/Api/Commands/Tag/GetSimilarCommand.cs new file mode 100644 index 00000000..80c469e6 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Tag/GetSimilarCommand.cs @@ -0,0 +1,47 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Tag +{ + [ApiMethodName("tag.getSimilar")] + internal class GetSimilarCommand : GetAsyncCommandBase> + { + public string TagName { get; set; } + + public GetSimilarCommand(ILastAuth auth, string tagname) + : base(auth) + { + TagName = tagname; + } + + public override void SetParameters() + { + Parameters.Add("tag", TagName); + + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json).SelectToken("similartags"); + var itemsToken = jtoken.SelectToken("tag"); + var attrToken = jtoken.SelectToken("@attr"); + var relatedTag = attrToken.SelectToken("tag").Value(); + + return PageResponse.CreateSuccessResponse(itemsToken, jt => LastTag.ParseJToken(jt, relatedTag)); + } + + return LastResponse.CreateErrorResponse>(status); + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Tag/GetTopAlbumsCommand.cs b/IF.Lastfm.Core/Api/Commands/Tag/GetTopAlbumsCommand.cs new file mode 100644 index 00000000..aac843e2 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Tag/GetTopAlbumsCommand.cs @@ -0,0 +1,47 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Tag +{ + [ApiMethodName("tag.getTopAlbums")] + internal class GetTopAlbumsCommand: GetAsyncCommandBase> + { + public string TagName { get; set; } + + public GetTopAlbumsCommand(ILastAuth auth, string tagName) : base(auth) + { + TagName = tagName; + } + + public override void SetParameters() + { + Parameters.Add("tag", TagName); + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("topalbums"); + var itemsToken = resultsToken.SelectToken("album"); + + return PageResponse.CreateSuccessResponse(itemsToken, resultsToken, LastAlbum.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Tag/GetTopArtistsCommand.cs b/IF.Lastfm.Core/Api/Commands/Tag/GetTopArtistsCommand.cs new file mode 100644 index 00000000..2efc4da7 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Tag/GetTopArtistsCommand.cs @@ -0,0 +1,53 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Tag +{ + [ApiMethodName("tag.getTopArtists")] + internal class GetTopArtistsCommand : GetAsyncCommandBase> + { + public string TagName { get; set; } + + public GetTopArtistsCommand(ILastAuth auth, string tagName) : base(auth) + { + TagName = tagName; + } + + public override void SetParameters() + { + Parameters.Add("tag", TagName); + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("topartists"); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + if (string.IsNullOrEmpty(resultsToken.SelectToken("@attr.tag").Value())) + { + return PageResponse.CreateErrorResponse(LastResponseStatus.MissingParameters); + } + + var itemsToken = resultsToken.SelectToken("artist"); + + return PageResponse.CreateSuccessResponse(itemsToken, resultsToken, LastArtist.ParseJToken, LastPageResultsType.Attr); + } + else + { + // The tag api always returns a "valid" response, so + return PageResponse.CreateErrorResponse(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Tag/GetTopTagsCommand.cs b/IF.Lastfm.Core/Api/Commands/Tag/GetTopTagsCommand.cs new file mode 100644 index 00000000..99d0ff04 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Tag/GetTopTagsCommand.cs @@ -0,0 +1,40 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Tag +{ + [ApiMethodName("tag.getTopTags")] + public class GetTopTagsCommand : GetAsyncCommandBase> + { + public GetTopTagsCommand(ILastAuth auth) + : base(auth) + { + } + + public override void SetParameters() + { + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var itemsToken = jtoken.SelectToken("toptags.tag"); + + return PageResponse.CreateSuccessResponse(itemsToken, LastTag.ParseJToken); + } + + return PageResponse.CreateErrorResponse(status); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Tag/GetTopTracksCommand.cs b/IF.Lastfm.Core/Api/Commands/Tag/GetTopTracksCommand.cs new file mode 100644 index 00000000..9ca4b664 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Tag/GetTopTracksCommand.cs @@ -0,0 +1,47 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Tag +{ + [ApiMethodName("tag.getTopTracks")] + internal class GetTopTracksCommand : GetAsyncCommandBase> + { + public string TagName { get; set; } + + public GetTopTracksCommand(ILastAuth auth, string tagName) : base(auth) + { + TagName = tagName; + } + + public override void SetParameters() + { + Parameters.Add("tag", TagName); + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("toptracks"); + var itemsToken = resultsToken.SelectToken("track"); + + return PageResponse.CreateSuccessResponse(itemsToken, resultsToken, LastTrack.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Track/AddShoutCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/AddShoutCommand.cs new file mode 100644 index 00000000..14b9ad76 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/AddShoutCommand.cs @@ -0,0 +1,36 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.shout")] + internal class AddShoutCommand : PostAsyncCommandBase + { + public string Track { get; set; } + + public string Artist { get; set; } + + public string Message { get; set; } + + + public AddShoutCommand(ILastAuth auth, string track, string artist, string message) : base(auth) + { + Track = track; + Artist = artist; + Message = message; + } + + public override void SetParameters() + { + Parameters.Add("track", Track); + Parameters.Add("artist", Artist); + Parameters.Add("message", Message); + } + + public override async Task HandleResponse(HttpResponseMessage response) + { + return await LastResponse.HandleResponse(response); + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Track/GetInfoCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/GetInfoCommand.cs new file mode 100644 index 00000000..cf86f941 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/GetInfoCommand.cs @@ -0,0 +1,65 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.getInfo")] + internal class GetInfoCommand : GetAsyncCommandBase> + { + public string TrackMbid { get; set; } + + public string TrackName { get; set; } + + public string ArtistName { get; set; } + + public string Username { get; set; } + + public bool Autocorrect { get; set; } + + public GetInfoCommand(ILastAuth auth) : base(auth) { } + + public override void SetParameters() + { + if (TrackMbid != null) + { + Parameters.Add("mbid", TrackMbid); + } + else + { + Parameters.Add("track", TrackName); + Parameters.Add("artist", ArtistName); + } + + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + if (!string.IsNullOrWhiteSpace(Username)) + { + Parameters.Add("username", Username); + } + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var track = LastTrack.ParseJToken(jtoken.SelectToken("track")); + + return LastResponse.CreateSuccessResponse(track); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Track/GetShoutsCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/GetShoutsCommand.cs new file mode 100644 index 00000000..2bd560fb --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/GetShoutsCommand.cs @@ -0,0 +1,57 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.getShouts")] + internal class GetShoutsCommand : GetAsyncCommandBase> + { + public string TrackName { get; set; } + + public string ArtistName { get; set; } + + public bool Autocorrect { get; set; } + + public GetShoutsCommand(ILastAuth auth, string trackname, string artistname) + : base(auth) + { + TrackName = trackname; + ArtistName = artistname; + } + + public override void SetParameters() + { + Parameters.Add("track", TrackName); + Parameters.Add("artist", ArtistName); + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json).SelectToken("shouts"); + var itemsToken = jtoken.SelectToken("shout"); + var pageInfoToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastShout.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Track/GetSimilarCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/GetSimilarCommand.cs new file mode 100644 index 00000000..d56496c8 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/GetSimilarCommand.cs @@ -0,0 +1,63 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.getSimilar")] + internal class GetSimilarCommand : GetAsyncCommandBase> + { + public string ArtistName { get; set; } + + public int? Limit { get; set; } + + public bool Autocorrect { get; set; } + + public string TrackName { get; set; } + + public GetSimilarCommand(ILastAuth auth, string trackName, string artistName) + : base(auth) + { + ArtistName = artistName; + TrackName = trackName; + } + + public override void SetParameters() + { + Parameters.Add("track", TrackName); + Parameters.Add("artist", ArtistName); + + if (Limit != null) + { + Parameters.Add("limit", Limit.ToString()); + } + + Parameters.Add("autocorrect", Convert.ToInt32(Autocorrect).ToString()); + + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var itemsToken = jtoken.SelectToken("similartracks").SelectToken("track"); + + return PageResponse.CreateSuccessResponse(itemsToken, LastTrack.ParseJToken); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Track/LoveCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/LoveCommand.cs new file mode 100644 index 00000000..19d52592 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/LoveCommand.cs @@ -0,0 +1,32 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.love")] + internal class LoveCommand : PostAsyncCommandBase + { + public string TrackName { get; set; } + + public string ArtistName { get; set; } + + public LoveCommand(ILastAuth auth, string trackname, string artistname) + : base(auth) + { + TrackName = trackname; + ArtistName = artistname; + } + + public override void SetParameters() + { + Parameters.Add("track", TrackName); + Parameters.Add("artist", ArtistName); + } + + public override async Task HandleResponse(HttpResponseMessage response) + { + return await LastResponse.HandleResponse(response); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Track/ScrobbleCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/ScrobbleCommand.cs new file mode 100644 index 00000000..ce04846d --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/ScrobbleCommand.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using IF.Lastfm.Core.Scrobblers; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.scrobble")] + internal class ScrobbleCommand : PostAsyncCommandBase + { + public IList Scrobbles { get; private set; } + + public ScrobbleCommand(ILastAuth auth, IList scrobbles) + : base(auth) + { + if (scrobbles.Count > 50) + { + throw new ArgumentOutOfRangeException("scrobbles", "Only 50 scrobbles can be sent at a time"); + } + + Scrobbles = scrobbles; + } + + protected override Uri BuildRequestUrl() + { + return new Uri(LastFm.ApiRootSsl, UriKind.Absolute); + } + + public ScrobbleCommand(ILastAuth auth, Scrobble scrobble) + : this(auth, new []{scrobble}) + { + } + + public override void SetParameters() + { + for(int i = 0; i < Scrobbles.Count; i++) + { + var scrobble = Scrobbles[i]; + + Parameters.Add(String.Format("artist[{0}]", i), scrobble.Artist); + Parameters.Add(String.Format("album[{0}]", i), scrobble.Album); + Parameters.Add(String.Format("track[{0}]", i), scrobble.Track); + Parameters.Add(String.Format("albumArtist[{0}]", i), scrobble.AlbumArtist); + Parameters.Add(String.Format("chosenByUser[{0}]", i), Convert.ToInt32(scrobble.ChosenByUser).ToString()); + Parameters.Add(String.Format("timestamp[{0}]", i), scrobble.TimePlayed.AsUnixTime().ToString()); + } + } + + public override async Task HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + return await ScrobbleResponse.CreateSuccessResponse(json); + } + else + { + return LastResponse.CreateErrorResponse(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Track/SearchCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/SearchCommand.cs new file mode 100644 index 00000000..248a013a --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/SearchCommand.cs @@ -0,0 +1,54 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.search")] + internal class SearchCommand : GetAsyncCommandBase> + { + public string TrackName { get; set; } + public string ArtistName { get; set; } + + public SearchCommand(ILastAuth auth, string trackName, string artistName = "") + : base(auth) + { + TrackName = trackName; + ArtistName = artistName; + } + + public override void SetParameters() + { + Parameters.Add("track", TrackName); + if (ArtistName.Length > 0) { + Parameters.Add("artist ", ArtistName); + } + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("results"); + var itemsToken = resultsToken.SelectToken("trackmatches").SelectToken("track"); + + return PageResponse.CreateSuccessResponse(itemsToken, resultsToken, LastTrack.ParseJToken, LastPageResultsType.OpenQuery); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/Track/UnloveCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/UnloveCommand.cs new file mode 100644 index 00000000..4aac60a9 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/UnloveCommand.cs @@ -0,0 +1,32 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.unlove")] + internal class UnloveCommand : PostAsyncCommandBase + { + public string TrackName { get; set; } + + public string ArtistName { get; set; } + + public UnloveCommand(ILastAuth auth, string trackname, string artistname) + : base(auth) + { + TrackName = trackname; + ArtistName = artistname; + } + + public override void SetParameters() + { + Parameters.Add("track", TrackName); + Parameters.Add("artist", ArtistName); + } + + public override async Task HandleResponse(HttpResponseMessage response) + { + return await LastResponse.HandleResponse(response); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/Track/UpdateNowPlayingCommand.cs b/IF.Lastfm.Core/Api/Commands/Track/UpdateNowPlayingCommand.cs new file mode 100644 index 00000000..ef28a1b5 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/Track/UpdateNowPlayingCommand.cs @@ -0,0 +1,58 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api.Commands.Track +{ + [ApiMethodName("track.updateNowPlaying")] + internal class UpdateNowPlayingCommand : PostAsyncCommandBase + { + public string Artist { get; set; } + + public string Album { get; set; } + + public string Track { get; set; } + + public string AlbumArtist { get; set; } + + public bool ChosenByUser { get; set; } + + public TimeSpan? Duration { get; set; } + + public UpdateNowPlayingCommand(ILastAuth auth, string artist, string album, string track) + : base(auth) + { + Artist = artist; + Album = album; + Track = track; + } + + public UpdateNowPlayingCommand(ILastAuth auth, Scrobble scrobble) + : this(auth, scrobble.Artist, scrobble.Album, scrobble.Track) + { + ChosenByUser = scrobble.ChosenByUser; + Duration = scrobble.Duration; + } + + public override void SetParameters() + { + Parameters.Add("artist", Artist); + Parameters.Add("album", Album); + Parameters.Add("track", Track); + Parameters.Add("albumArtist", AlbumArtist); + Parameters.Add("chosenByUser", Convert.ToInt32(ChosenByUser).ToString()); + + if (Duration.HasValue) + { + Parameters.Add("duration",Math.Round(Duration.Value.TotalSeconds).ToString()); + } + } + + public override Task HandleResponse(HttpResponseMessage response) + { + return LastResponse.HandleResponse(response); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/UnauthenticatedPostAsyncCommandBase.cs b/IF.Lastfm.Core/Api/Commands/UnauthenticatedPostAsyncCommandBase.cs new file mode 100644 index 00000000..3af8b803 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/UnauthenticatedPostAsyncCommandBase.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands +{ + internal abstract class UnauthenticatedPostAsyncCommandBase : PostAsyncCommandBase where T : LastResponse, new() + { + protected UnauthenticatedPostAsyncCommandBase(ILastAuth auth) : base(auth) + { + } + + public override Task ExecuteAsync() + { + return ExecuteAsyncInternal(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/AddShoutCommand.cs b/IF.Lastfm.Core/Api/Commands/User/AddShoutCommand.cs new file mode 100644 index 00000000..37019a4b --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/AddShoutCommand.cs @@ -0,0 +1,32 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.shout")] + internal class AddShoutCommand : PostAsyncCommandBase + { + public string Recipient { get; set; } + + public string Message { get; set; } + + public AddShoutCommand(ILastAuth auth, string recipient, string message) + : base(auth) + { + Recipient = recipient; + Message = message; + } + + public override void SetParameters() + { + Parameters.Add("user", Recipient); + Parameters.Add("message", Message); + } + + public override async Task HandleResponse(HttpResponseMessage response) + { + return await LastResponse.HandleResponse(response); + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/User/GetInfoCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetInfoCommand.cs new file mode 100644 index 00000000..5346d469 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetInfoCommand.cs @@ -0,0 +1,47 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getInfo")] + internal class GetInfoCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + + public GetInfoCommand(ILastAuth auth, string username) : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var userToken = jtoken.SelectToken("user"); + var user = LastUser.ParseJToken(userToken); + + return LastResponse.CreateSuccessResponse(user); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/GetLovedTracksCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetLovedTracksCommand.cs new file mode 100644 index 00000000..48012f3e --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetLovedTracksCommand.cs @@ -0,0 +1,52 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getLovedTracks")] + internal class GetLovedTracksCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + public GetLovedTracksCommand(ILastAuth auth, string username) : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + + this.AddPagingParameters(); + this.DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json).SelectToken("lovedtracks"); + var itemsToken = jtoken.SelectToken("track"); + var attrToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse( + itemsToken, + attrToken, + LastTrack.ParseJToken, + LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/User/GetRecentStationsCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetRecentStationsCommand.cs new file mode 100644 index 00000000..73bd0be2 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetRecentStationsCommand.cs @@ -0,0 +1,55 @@ +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getRecentStations")] + internal class GetRecentStationsCommand : PostAsyncCommandBase> + { + public string Username { get; private set; } + + public GetRecentStationsCommand(ILastAuth auth, string username) : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + + AddPagingParameters(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + string json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + JToken jtoken = JsonConvert.DeserializeObject(json).SelectToken("recentstations"); + + var stationsToken = jtoken.SelectToken("station"); + + var stations = stationsToken.Children().Select(LastStation.ParseJToken).ToList(); + + var pageresponse = PageResponse.CreateSuccessResponse(stations); + + var attrToken = jtoken.SelectToken("@attr"); + pageresponse.AddPageInfoFromJToken(attrToken); + + return pageresponse; + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/GetRecentTracksCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetRecentTracksCommand.cs new file mode 100644 index 00000000..e2d38db6 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetRecentTracksCommand.cs @@ -0,0 +1,70 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getRecentTracks")] + internal class GetRecentTracksCommand : GetAsyncCommandBase> + { + public string Username { get; private set; } + + public DateTimeOffset? From { get; set; } + + public DateTimeOffset? To { get; set; } + + public bool? Extended { get; set; } + + public GetRecentTracksCommand(ILastAuth auth, string username) : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + + if (From.HasValue) + { + Parameters.Add("from", From.Value.AsUnixTime().ToString()); + } + + if (To.HasValue) + { + Parameters.Add("to", To.Value.AsUnixTime().ToString()); + } + + if (Extended.HasValue) + { + Parameters.Add("extended", Extended.ToString()); + } + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json).SelectToken("recenttracks"); + var itemsToken = jtoken.SelectToken("track"); + var attrToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, attrToken, LastTrack.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/GetRecommendedArtistsCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetRecommendedArtistsCommand.cs new file mode 100644 index 00000000..4e439e99 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetRecommendedArtistsCommand.cs @@ -0,0 +1,40 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getRecommendedArtists")] + internal class GetRecommendedArtistsCommand : PostAsyncCommandBase> + { + public GetRecommendedArtistsCommand(ILastAuth auth) : base(auth) { } + + public override void SetParameters() + { + AddPagingParameters(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var resultsToken = jtoken.SelectToken("recommendations"); + var itemsToken = resultsToken.SelectToken("artist"); + + return PageResponse.CreateSuccessResponse(itemsToken, resultsToken, LastArtist.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/GetShoutsCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetShoutsCommand.cs new file mode 100644 index 00000000..e924e325 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetShoutsCommand.cs @@ -0,0 +1,49 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getShouts")] + internal class GetShoutsCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + + public GetShoutsCommand(ILastAuth auth, string username) : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var shoutsToken = jtoken.SelectToken("shouts"); + var itemsToken = shoutsToken.SelectToken("shout"); + var pageInfoToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastShout.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/GetTopAlbumsCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetTopAlbumsCommand.cs new file mode 100644 index 00000000..7240c64f --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetTopAlbumsCommand.cs @@ -0,0 +1,51 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getTopAlbums")] + internal class GetTopAlbumsCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + public LastStatsTimeSpan TimeSpan { get; set; } + + public GetTopAlbumsCommand(ILastAuth auth, string username, LastStatsTimeSpan span) : base(auth) + { + Username = username; + TimeSpan = span; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + Parameters.Add("period", TimeSpan.GetApiName()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JToken.Parse(json); + var itemsToken = jtoken.SelectToken("topalbums").SelectToken("album"); + var pageInfoToken = jtoken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastAlbum.ParseJToken, LastPageResultsType.Attr); + } + else + { + return LastResponse.CreateErrorResponse>(status); + } + + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/GetTopArtistsCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetTopArtistsCommand.cs new file mode 100644 index 00000000..d36220fb --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetTopArtistsCommand.cs @@ -0,0 +1,51 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getTopArtists")] + internal class GetTopArtistsCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + public LastStatsTimeSpan TimeSpan { get; set; } + + public GetTopArtistsCommand(ILastAuth auth, string username, LastStatsTimeSpan span) + : base(auth) + { + Username = username; + TimeSpan = span; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + Parameters.Add("period", TimeSpan.GetApiName()); + + AddPagingParameters(); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var topArtistsToken = jtoken.SelectToken("topartists"); + var itemsToken = topArtistsToken.SelectToken("artist"); + var pageInfoToken = topArtistsToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastArtist.ParseJToken, LastPageResultsType.Attr); + } + + return LastResponse.CreateErrorResponse>(status); + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/User/GetWeeklyAlbumChartCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetWeeklyAlbumChartCommand.cs new file mode 100644 index 00000000..0aed6c76 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetWeeklyAlbumChartCommand.cs @@ -0,0 +1,56 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getWeeklyAlbumChart")] + internal class GetWeeklyAlbumChartCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + public double? From { get; set; } + public double? To { get; set; } + + public GetWeeklyAlbumChartCommand(ILastAuth auth, string username) + : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + if(From != null) + { + Parameters.Add("from", From.ToString()); + } + if(To != null) + { + Parameters.Add("to", To.ToString()); + } + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var albumListToken = jtoken.SelectToken("weeklyalbumchart"); + var itemsToken = albumListToken.SelectToken("album"); + var pageInfoToken = albumListToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastAlbum.ParseJToken, LastPageResultsType.Attr); + } + + return LastResponse.CreateErrorResponse>(status); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/GetWeeklyArtistChartCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetWeeklyArtistChartCommand.cs new file mode 100644 index 00000000..72a70357 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetWeeklyArtistChartCommand.cs @@ -0,0 +1,56 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getWeeklyArtistChart")] + internal class GetWeeklyArtistChartCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + public double? From { get; set; } + public double? To { get; set; } + + public GetWeeklyArtistChartCommand(ILastAuth auth, string username) + : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + if(From != null) + { + Parameters.Add("from", From.ToString()); + } + if(To != null) + { + Parameters.Add("to", To.ToString()); + } + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var chartListToken = jtoken.SelectToken("weeklyartistchart"); + var itemsToken = chartListToken.SelectToken("artist"); + var pageInfoToken = chartListToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastArtist.ParseJToken, LastPageResultsType.Attr); + } + + return LastResponse.CreateErrorResponse>(status); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Commands/User/GetWeeklyChartListCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetWeeklyChartListCommand.cs new file mode 100644 index 00000000..30a7cdf8 --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetWeeklyChartListCommand.cs @@ -0,0 +1,46 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getWeeklyChartList")] + internal class GetWeeklyChartListCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + + public GetWeeklyChartListCommand(ILastAuth auth, string username) + : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var chartListToken = jtoken.SelectToken("weeklychartlist"); + var itemsToken = chartListToken.SelectToken("chart"); + var pageInfoToken = chartListToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastWeeklyChartList.ParseJToken, LastPageResultsType.Attr); + } + + return LastResponse.CreateErrorResponse>(status); + } + } +} diff --git a/IF.Lastfm.Core/Api/Commands/User/GetWeeklyTrackChartCommand.cs b/IF.Lastfm.Core/Api/Commands/User/GetWeeklyTrackChartCommand.cs new file mode 100644 index 00000000..fe9231fb --- /dev/null +++ b/IF.Lastfm.Core/Api/Commands/User/GetWeeklyTrackChartCommand.cs @@ -0,0 +1,56 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Commands.User +{ + [ApiMethodName("user.getWeeklyTrackChart")] + internal class GetWeeklyTrackChartCommand : GetAsyncCommandBase> + { + public string Username { get; set; } + public double? From { get; set; } + public double? To { get; set; } + + public GetWeeklyTrackChartCommand(ILastAuth auth, string username) + : base(auth) + { + Username = username; + } + + public override void SetParameters() + { + Parameters.Add("user", Username); + if(From != null) + { + Parameters.Add("from", From.ToString()); + } + if(To != null) + { + Parameters.Add("to", To.ToString()); + } + DisableCaching(); + } + + public override async Task> HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + var jtoken = JsonConvert.DeserializeObject(json); + var chartListToken = jtoken.SelectToken("weeklytrackchart"); + var itemsToken = chartListToken.SelectToken("track"); + var pageInfoToken = chartListToken.SelectToken("@attr"); + + return PageResponse.CreateSuccessResponse(itemsToken, pageInfoToken, LastTrack.ParseJToken, LastPageResultsType.Attr); + } + + return LastResponse.CreateErrorResponse>(status); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Enums/LastResponseStatus.cs b/IF.Lastfm.Core/Api/Enums/LastResponseStatus.cs new file mode 100644 index 00000000..e03acc70 --- /dev/null +++ b/IF.Lastfm.Core/Api/Enums/LastResponseStatus.cs @@ -0,0 +1,99 @@ +namespace IF.Lastfm.Core.Api.Enums +{ + public enum LastResponseStatus + { + Unknown = 0, + + /// + /// The request was successful! + /// + Successful = 20, + + /// + /// The request has been cached, it will be sent later + /// + Cached = 21, + + /// + /// The request could not be sent, and could not be cached. + /// Check the Exception property of the response for details. + /// + CacheFailed = 22, + + /// + /// The request failed, check for network connectivity + /// + RequestFailed = 23, + + /// + /// The service requested does not exist (2) + /// + BadService = 2, + + /// + /// The method requested does not exist in this service (3) + /// + BadMethod = 3, + + /// + /// This credential does not have permission to access the service requested (4) + /// + BadAuth = 4, + + /// + /// This service doesn't exist in the requested format + /// + BadFormat = 5, + + /// + /// Required parameters were missing from the request (6) + /// + MissingParameters = 6, + + /// + /// The requested resource is invalid (7) + /// + BadResource = 7, + + /// + /// An unknown failure occured when creating the response (8) + /// + Failure = 8, + + /// + /// The session has expired, reauthenticate before retrying (9) + /// + SessionExpired = 9, + + /// + /// The provided API key was invalid (10) + /// + BadApiKey = 10, + + /// + /// This service is temporarily offline, retry later (11) + /// + ServiceDown = 11, + + /// + /// The request signature was invalid. Check that your API key and secret are valid. (13) + /// You can generate new keys at http://www.last.fm/api/accounts + /// + BadMethodSignature = 13, + + /// + /// There was a temporary error while processing the request, retry later (16) + /// + TemporaryFailure = 16, + + /// + /// This API key has been suspended, please generate a new key at http://www.last.fm/api/accounts (26) + /// + KeySuspended = 26, + + /// + /// This API key has been rate-limited because too many requests have been made in a short period. Retry later (29) + /// + RateLimited = 29 + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Enums/LastStatsTimeSpan.cs b/IF.Lastfm.Core/Api/Enums/LastStatsTimeSpan.cs new file mode 100644 index 00000000..6ae51ffc --- /dev/null +++ b/IF.Lastfm.Core/Api/Enums/LastStatsTimeSpan.cs @@ -0,0 +1,39 @@ +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Api.Enums +{ + public enum LastStatsTimeSpan + { + [ApiName("overall")] + Overall = 0, + + [ApiName("7day")] + Week, + + [ApiName("1month")] + Month, + + [ApiName("3month")] + Quarter, + + [ApiName("6month")] + Half, + + [ApiName("12month")] + Year + } + + public enum Gender + { + Other = 0, + Male, + Female + } + + public enum LastPageResultsType + { + None = 0, + Attr, + OpenQuery + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Helpers/ApiExtensions.cs b/IF.Lastfm.Core/Api/Helpers/ApiExtensions.cs new file mode 100644 index 00000000..ac81f822 --- /dev/null +++ b/IF.Lastfm.Core/Api/Helpers/ApiExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace IF.Lastfm.Core.Api.Helpers +{ + public static class ApiExtensions + { + public static DateTimeOffset UnixEpoch + { + get { return new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); } + } + + public static T GetAttribute(this Enum enumValue) + where T : Attribute + { + return enumValue + .GetType() + .GetTypeInfo() + .GetDeclaredField(enumValue.ToString()) + .GetCustomAttribute(); + } + + public static string GetApiName(this Enum enumValue) + { + var attribute = enumValue.GetAttribute(); + + return (attribute != null && !string.IsNullOrWhiteSpace(attribute.Text)) + ? attribute.Text + : enumValue.ToString(); + } + + public static int AsUnixTime(this DateTimeOffset dt) + { + var d = (dt - UnixEpoch).TotalSeconds; + + return Convert.ToInt32(d); + } + + public static DateTimeOffset FromUnixTime(this double stamp) + { + var d = UnixEpoch.AddSeconds(stamp); + return d; + } + + public static int CountOrDefault(this IEnumerable enumerable) + { + return enumerable != null + ? enumerable.Count() + : 0; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Helpers/ApiNameAttribute.cs b/IF.Lastfm.Core/Api/Helpers/ApiNameAttribute.cs new file mode 100644 index 00000000..d391fe80 --- /dev/null +++ b/IF.Lastfm.Core/Api/Helpers/ApiNameAttribute.cs @@ -0,0 +1,25 @@ +using IF.Lastfm.Core.Api.Commands; +using System; + +namespace IF.Lastfm.Core.Api.Helpers +{ + public class ApiNameAttribute : Attribute + { + public string Text { get; private set; } + + public ApiNameAttribute(string name) + { + Text = name; + } + } + + /// + /// This attribute defines the api method name (i.e: "album.getInfo") for a Command. + /// When applied on a implementation, the property is set to the attribute value. + /// + [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)] + public class ApiMethodNameAttribute : ApiNameAttribute + { + public ApiMethodNameAttribute(string name) : base(name) { } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Helpers/LastResponse.cs b/IF.Lastfm.Core/Api/Helpers/LastResponse.cs new file mode 100644 index 00000000..19eaf6b2 --- /dev/null +++ b/IF.Lastfm.Core/Api/Helpers/LastResponse.cs @@ -0,0 +1,62 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; + +namespace IF.Lastfm.Core.Api.Helpers +{ + public interface ILastResponse + { + bool Success { get; } + + LastResponseStatus Status { get; } + } + + public class LastResponse : ILastResponse + { + public virtual bool Success + { + get { return Status == LastResponseStatus.Successful; } + } + + public LastResponseStatus Status { get; internal set; } + + [Obsolete("This property has been renamed to Status and will be removed soon.")] + public LastResponseStatus Error { get { return Status; } } + + public static LastResponse CreateSuccessResponse() + { + var r = new LastResponse + { + Status = LastResponseStatus.Successful + }; + + return r; + } + + public static T CreateErrorResponse(LastResponseStatus status) where T : LastResponse, new() + { + var r = new T + { + Status = status + }; + + return r; + } + + public static async Task HandleResponse(HttpResponseMessage response) + { + var json = await response.Content.ReadAsStringAsync(); + + LastResponseStatus status; + if (LastFm.IsResponseValid(json, out status) && response.IsSuccessStatusCode) + { + return LastResponse.CreateSuccessResponse(); + } + else + { + return LastResponse.CreateErrorResponse(status); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Helpers/LastResponse{T}.cs b/IF.Lastfm.Core/Api/Helpers/LastResponse{T}.cs new file mode 100644 index 00000000..d36e4e3f --- /dev/null +++ b/IF.Lastfm.Core/Api/Helpers/LastResponse{T}.cs @@ -0,0 +1,20 @@ +using IF.Lastfm.Core.Api.Enums; + +namespace IF.Lastfm.Core.Api.Helpers +{ + public class LastResponse : LastResponse + { + public T Content { get; set; } + + public static LastResponse CreateSuccessResponse(T content) + { + var r = new LastResponse + { + Content = content, + Status = LastResponseStatus.Successful + }; + + return r; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/Helpers/PageResponse.cs b/IF.Lastfm.Core/Api/Helpers/PageResponse.cs new file mode 100644 index 00000000..820cc8b1 --- /dev/null +++ b/IF.Lastfm.Core/Api/Helpers/PageResponse.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Api.Helpers +{ + public interface IPageResponse : ILastResponse, IEnumerable where T : new() + { + IReadOnlyList Content { get; } + + int Page { get; } + + int PageSize { get; } + + int TotalPages { get; } + + int TotalItems { get; } + } + + [JsonConverter(typeof(PageResponseJsonConverter))] + public class PageResponse : LastResponse, IPageResponse where T : new() + { + private int? _totalItems; + private int? _pageSize; + + public PageResponse() : this(Enumerable.Empty()) + { + } + + public PageResponse(IEnumerable content) + { + Page = 1; + TotalPages = 1; + Content = new ReadOnlyCollection(content.ToList()); + } + + public IReadOnlyList Content { get; internal set; } + + public int Page { get; internal set; } + + public int TotalPages { get; internal set; } + + public int TotalItems + { + get { return _totalItems ?? Content.CountOrDefault(); } + internal set { _totalItems = value; } + } + + public int PageSize + { + get { return _pageSize ?? Content.CountOrDefault(); } + internal set { _pageSize = value; } + } + + public IEnumerator GetEnumerator() + { + return Content != null + ? Content.GetEnumerator() + : null; + } + + IEnumerator IEnumerable.GetEnumerator() + { + if (Content != null) + { + return Content.GetEnumerator(); + } + else + { + return null; + } + } + + public static PageResponse CreateErrorResponse(LastResponseStatus status) + { + var r = new PageResponse + { + Status = status + }; + + r.AddDefaultPageInfo(); + + return r; + } + + + public new static PageResponse CreateSuccessResponse() + { + var r = new PageResponse + { + Status = LastResponseStatus.Successful + }; + + r.AddDefaultPageInfo(); + + return r; + } + + [Obsolete] + public static PageResponse CreateSuccessResponse(IEnumerable content) + { + var r = new PageResponse(content) + { + Status = LastResponseStatus.Successful + }; + + return r; + } + + public static PageResponse CreateSuccessResponse(JToken itemsToken, Func parseToken) + { + return CreateSuccessResponse(itemsToken, null, parseToken, LastPageResultsType.None); + } + + public static IEnumerable ParseItemsToken(JToken itemsToken, Func parseToken) + { + IEnumerable items; + if (itemsToken != null && itemsToken.Children().Any()) + { + // array notation isn't used on the api when only one object is available + if (itemsToken.Type == JTokenType.Object) + { + items = new[] { parseToken(itemsToken) }; + } + else if (itemsToken.Type == JTokenType.Array) + { + items = itemsToken.Children().Select(parseToken); + } + else + { + throw new ArgumentException(String.Format("Couldn't parse items token\r\n\r\n{0}", itemsToken.ToString())); + } + } + else + { + items = Enumerable.Empty(); + } + + return items; + } + + public static PageResponse CreateSuccessResponse(JToken itemsToken, JToken pageInfoToken, Func parseToken, LastPageResultsType pageResultsType) + { + var items = ParseItemsToken(itemsToken, parseToken); + + var pageresponse = new PageResponse(items) + { + Status = LastResponseStatus.Successful + }; + + switch (pageResultsType) + { + case LastPageResultsType.Attr: + pageresponse.AddPageInfoFromJToken(pageInfoToken); + break; + case LastPageResultsType.OpenQuery: + pageresponse.AddPageInfoFromOpenQueryJToken(pageInfoToken); + break; + case LastPageResultsType.None: + default: + pageresponse.AddDefaultPageInfo(pageresponse.Content); + break; + } + + return pageresponse; + } + + private void AddDefaultPageInfo() + { + AddDefaultPageInfo(Enumerable.Empty().ToList()); + } + + private void AddDefaultPageInfo(IReadOnlyCollection items) + { + Page = 1; + TotalPages = 1; + TotalItems = items.Count; + PageSize = items.Count; + } + + internal void AddPageInfoFromJToken(JToken attrToken) + { + if (attrToken == null) + { + return; + } + + var page = attrToken.Value("page"); + Page = !string.IsNullOrWhiteSpace(page) ? Convert.ToInt32(page) : 1; + + var totalPages = attrToken.Value("totalPages"); + TotalPages = !string.IsNullOrWhiteSpace(totalPages) ? Convert.ToInt32(totalPages) : 1; + + var totalItems = attrToken.Value("total"); + TotalItems = !string.IsNullOrWhiteSpace(totalItems) ? Convert.ToInt32(totalItems) : 1; + + var pagesize = attrToken.Value("perPage"); + PageSize = !string.IsNullOrWhiteSpace(pagesize) ? Convert.ToInt32(pagesize) : 1; + } + + private void AddPageInfoFromOpenQueryJToken(JToken queryToken) + { + if (queryToken == null) + { + return; + } + + var page = queryToken.SelectToken("opensearch:Query").Value("startPage"); + Page = !string.IsNullOrWhiteSpace(page) ? Convert.ToInt32(page) : 1; + + var totalItems = queryToken.Value("opensearch:totalResults"); + TotalItems = !string.IsNullOrWhiteSpace(totalItems) ? Convert.ToInt32(totalItems) : 1; + + var pagesize = queryToken.Value("opensearch:itemsPerPage"); + PageSize = !string.IsNullOrWhiteSpace(pagesize) ? Convert.ToInt32(pagesize) : 1; + + // the response doesn't include total pages, bit of improv then. + TotalPages = (int)Math.Ceiling((double)TotalItems / PageSize); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/IAlbumApi.cs b/IF.Lastfm.Core/Api/IAlbumApi.cs new file mode 100644 index 00000000..1a1743ea --- /dev/null +++ b/IF.Lastfm.Core/Api/IAlbumApi.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public interface IAlbumApi + { + ILastAuth Auth { get; } + + Task> GetInfoAsync(string artist, string album, bool autocorrect = false, string username = null); + + Task> GetInfoByMbidAsync(string albumMbid, bool autocorrect = false, string username = null); + + //Task> GetBuyLinksForAlbumAsync(string artist, + // string album, + // CountryCode country, + // bool autocorrect = false); + + Task> GetTagsByUserAsync(string artist, + string album, + string username, + bool autocorrect = false); + + Task> GetTopTagsAsync(string artist, + string album, + bool autocorrect = false); + + Task> SearchAsync(string album, + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + Task> GetShoutsAsync(string albumname, + string artistname, + bool autocorrect = false, + int page = 1, + int count = LastFm.DefaultPageLength); + + //Task AddShoutAsync(string albumname, string artistname, string message); + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/IArtistApi.cs b/IF.Lastfm.Core/Api/IArtistApi.cs new file mode 100644 index 00000000..65b460f8 --- /dev/null +++ b/IF.Lastfm.Core/Api/IArtistApi.cs @@ -0,0 +1,57 @@ +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public interface IArtistApi + { + ILastAuth Auth { get; } + + Task> GetInfoAsync(string artist, string bioLang = LastFm.DefaultLanguageCode, + bool autocorrect = false); + + Task> GetInfoByMbidAsync(string mbid, string bioLang = LastFm.DefaultLanguageCode, + bool autocorrect = false); + + Task> GetSimilarAsync(string artistname, bool autocorrect = false, int limit = 100); + + Task> GetSimilarByMbidAsync(string mbid, bool autocorrect = false, int limit = 100); + + Task> GetTopAlbumsAsync(string artist, + bool autocorrect = false, + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + Task> GetTopAlbumsByMbidAsync(string mbid, + bool autocorrect = false, + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + + Task> GetTopTracksAsync(string artist, + bool autocorrect = false, + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + Task> GetTagsByUserAsync(string artist, + string username, + bool autocorrect = false, + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + Task> GetTopTagsAsync(string artist, bool autocorrect = false); + + Task> GetShoutsAsync(string artistname, + int page = 0, + int count = LastFm.DefaultPageLength, + bool autocorrect = false); + + Task AddShoutAsync(string artistname, string message); + + Task> SearchAsync(string artistname, + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + } +} diff --git a/IF.Lastfm.Core/Api/IChartApi.cs b/IF.Lastfm.Core/Api/IChartApi.cs new file mode 100644 index 00000000..79a03e81 --- /dev/null +++ b/IF.Lastfm.Core/Api/IChartApi.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public interface IChartApi + { + ILastAuth Auth { get; } + + Task> GetTopArtistsAsync( + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + Task> GetTopTracksAsync( + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + } +} diff --git a/IF.Lastfm.Core/Api/ILastAuth.cs b/IF.Lastfm.Core/Api/ILastAuth.cs new file mode 100644 index 00000000..c89809d0 --- /dev/null +++ b/IF.Lastfm.Core/Api/ILastAuth.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public interface ILastAuth + { + bool Authenticated { get; } + string ApiKey { get; } + LastUserSession UserSession { get; } + + /// + /// Load an existing user session object. + /// + /// + /// Whether session object is valid + bool LoadSession(LastUserSession session); + + /// + /// Gets the session token which is used as authentication for any service calls. + /// Username and password aren't stored. + /// + /// Username + /// UserSession's password + /// Session token used to authenticate calls to last.fm + /// API: Auth.getMobileSession + Task GetSessionTokenAsync(string username, string password); + + /// + /// Gets the session token which is used as authentication for any service calls. + /// Authentication Token from the Web Authentication 3.1 (https://www.last.fm/api/webauth) + /// + /// Authentication Token + /// Session token used to authenticate calls to Last.fm + /// API: Auth.getSession + Task GetSessionTokenAsync(string authToken); + + /// + /// Fetch an unathorized request token for an API account. + /// This is step 2 of the authentication process for desktop applications. (https://www.last.fm/api/desktopauth) + /// + /// Authentication Token used to get Last.fm Session. + /// Authentication tokens are user and API account specific. They are valid for 60 minutes from the moment they are granted. + /// API: Auth.getToken + Task GetAuthTokenAsync(); + + /// + /// Adds the api_key, method and session key to the provided params dictionary, then generates an MD5 hash. + /// Parameters contained in the hash must also be exactly the parameters sent to the API. + /// + string GenerateMethodSignature(string method, Dictionary parameters = null); + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/ILibraryAPI.cs b/IF.Lastfm.Core/Api/ILibraryAPI.cs new file mode 100644 index 00000000..9f9dc67f --- /dev/null +++ b/IF.Lastfm.Core/Api/ILibraryAPI.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public interface ILibraryApi + { + ILastAuth Auth { get; } + + Task> GetArtists( + string username, + DateTimeOffset since, + int startIndex = 0, + int endIndex = LastFm.DefaultPageLength); + + Task> GetTracks( + string username, + string artist, + string album, + DateTimeOffset since, + int startIndex = 0, + int endIndex = LastFm.DefaultPageLength); + + Task RemoveScrobble( + string artist, + string track, + DateTimeOffset timestamp); + + Task RemoveTrack( + string artist, + string track); + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/ITagApi.cs b/IF.Lastfm.Core/Api/ITagApi.cs new file mode 100644 index 00000000..69c05c29 --- /dev/null +++ b/IF.Lastfm.Core/Api/ITagApi.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public interface ITagApi + { + Task> GetSimilarAsync(string tagName); + Task> GetInfoAsync(string tagName); + Task> GetTopAlbumsAsync(string tagName,int page,int itemsPerPage); + Task> GetTopArtistsAsync(string tagName, int page, int itemsPerPage); + Task> GetTopTracksAsync(string tagName, int page, int itemsPerPage); + Task> GetTopTagsAsync(); + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/ITrackApi.cs b/IF.Lastfm.Core/Api/ITrackApi.cs new file mode 100644 index 00000000..870af5da --- /dev/null +++ b/IF.Lastfm.Core/Api/ITrackApi.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using IF.Lastfm.Core.Scrobblers; + +namespace IF.Lastfm.Core.Api +{ + public interface ITrackApi + { + ILastAuth Auth { get; } + + [Obsolete("This method has been moved to the Scrobbler class. More information can be found at " + LastFm.SCROBBLING_HELP_URL)] + Task ScrobbleAsync(Scrobble scrobble); + //Task ScrobbleAsync(IEnumerable scrobble); + + Task UpdateNowPlayingAsync(Scrobble scrobble); + + Task> GetShoutsForTrackAsync(string trackname, + string artistname, + bool autocorrect = false, + int page = 0, + int count = LastFm.DefaultPageLength); + + Task> GetInfoAsync(string trackname, string artistname, string username = ""); + Task> GetInfoByMbidAsync(string mbid); + Task> GetSimilarAsync(string trackname, string artistname, bool autocorrect = false, int limit = 100); + + Task LoveAsync(string trackname, string artistname); + Task UnloveAsync(string trackname, string artistname); + + Task> SearchAsync(string trackname, + string artistname = "", + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + //Task AddShoutAsync(string trackname, string artistname, string message); + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/IUserApi.cs b/IF.Lastfm.Core/Api/IUserApi.cs new file mode 100644 index 00000000..26addec2 --- /dev/null +++ b/IF.Lastfm.Core/Api/IUserApi.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public interface IUserApi + { + ILastAuth Auth { get; } + + Task> GetRecommendedArtistsAsync( + int page = 1, + int itemsPerPage = LastFm.DefaultPageLength); + + Task> GetTopAlbums(string username, + LastStatsTimeSpan span, + int startIndex = 0, + int endIndex = LastFm.DefaultPageLength); + + Task> GetTopArtists(string username, + LastStatsTimeSpan span, + int pagenumber = 0, + int count = LastFm.DefaultPageLength); + + Task> GetRecentScrobbles(string username, DateTimeOffset? from = null, + DateTimeOffset? to = null, bool extendedResponse = false, int pagenumber = LastFm.DefaultPage, + int count = LastFm.DefaultPageLength); + + Task> GetRecentStations(string username, + int pagenumber, + int count = LastFm.DefaultPageLength); + + Task> GetShoutsAsync(string username, + int pagenumber, + int count = LastFm.DefaultPageLength); + + Task> GetInfoAsync(string username); + + Task AddShoutAsync(string recipient, string message); + + Task> GetLovedTracks(string username, int pagenumber, int count); + + Task> GetWeeklyChartListAsync(string username); + + Task> GetWeeklyArtistChartAsync(string username, double? to = null, double? from = null); + + Task> GetWeeklyTrackChartAsync(string username, double? to = null, double? from = null); + + Task> GetWeeklyAlbumChartAsync(string username, double? to = null, double? from = null); + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/LastAuth.cs b/IF.Lastfm.Core/Api/LastAuth.cs new file mode 100644 index 00000000..730581c8 --- /dev/null +++ b/IF.Lastfm.Core/Api/LastAuth.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Commands.Auth; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public class LastAuth : ApiBase, ILastAuth + { + private readonly string _apiSecret; + + public bool Authenticated { get { return UserSession != null; } } + public string ApiKey { get; private set; } + public LastUserSession UserSession { get; private set; } + + public LastAuth(string apikey, string secret, HttpClient httpClient = null) + : base(httpClient) + { + ApiKey = apikey; + _apiSecret = secret; + Auth = this; + } + + /// + /// Load an existing user session + /// + /// Session to load + /// Whether session object is valid + public bool LoadSession(LastUserSession session) + { + UserSession = session; + return true; + } + + public async Task GetSessionTokenAsync(string username, string password) + { + var command = new GetMobileSessionCommand(this, username, password) + { + HttpClient = HttpClient + }; + var response = await command.ExecuteAsync(); + + if (response.Success) + { + UserSession = response.Content; + return LastResponse.CreateSuccessResponse(); + } + else + { + return LastResponse.CreateErrorResponse(response.Status); + } + } + + public async Task GetSessionTokenAsync(string authToken) + { + var command = new GetSessionCommand(this, authToken) + { + HttpClient = HttpClient + }; + var response = await command.ExecuteAsync(); + + if (response.Success) + { + UserSession = response.Content; + return LastResponse.CreateSuccessResponse(); + } + else + { + return LastResponse.CreateErrorResponse(response.Status); + } + } + + public async Task GetAuthTokenAsync() + { + var command = new GetTokenCommand(this) + { + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public string GenerateMethodSignature(string method, Dictionary parameters = null) + { + if (parameters == null) + { + parameters = new Dictionary(); + } + + parameters.Add("api_key", ApiKey); + parameters.Add("method", method); + if (Authenticated) + { + parameters.Add("sk", UserSession.Token); + } + + var builder = new StringBuilder(); + + foreach (var kv in parameters.OrderBy(kv => kv.Key, StringComparer.Ordinal)) + { + builder.Append(kv.Key); + builder.Append(kv.Value); + } + + builder.Append(_apiSecret); + + var md5 = MD5.GetHashString(builder.ToString()); + + return md5; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/LastfmClient.cs b/IF.Lastfm.Core/Api/LastfmClient.cs new file mode 100644 index 00000000..2300da69 --- /dev/null +++ b/IF.Lastfm.Core/Api/LastfmClient.cs @@ -0,0 +1,59 @@ +using System.Net.Http; +using IF.Lastfm.Core.Helpers; +using IF.Lastfm.Core.Scrobblers; + +namespace IF.Lastfm.Core.Api +{ + public class LastfmClient : ApiBase + { + private AlbumApi _albumApi; + private ArtistApi _artistApi; + private ChartApi _chartApi; + private LibraryApi _libraryApi; + private ScrobblerBase _scrobbler; + private TagApi _tagApi; + private TrackApi _trackApi; + private UserApi _userApi; + + public AlbumApi Album => _albumApi ?? (_albumApi = new AlbumApi(Auth, HttpClient)); + public ArtistApi Artist => _artistApi ?? (_artistApi = new ArtistApi(Auth, HttpClient)); + public ChartApi Chart => _chartApi ?? (_chartApi = new ChartApi(Auth, HttpClient)); + public LibraryApi Library => _libraryApi ?? (_libraryApi = new LibraryApi(Auth, HttpClient)); + public TagApi Tag => _tagApi ?? (_tagApi = new TagApi(Auth, HttpClient)); + public TrackApi Track => _trackApi ?? (_trackApi = new TrackApi(Auth, HttpClient)); + public UserApi User => _userApi ?? (_userApi = new UserApi(Auth, HttpClient)); + + public ScrobblerBase Scrobbler + { + get { return _scrobbler ?? (_scrobbler = new MemoryScrobbler(Auth, HttpClient)); } + set { _scrobbler = value; } + } + + public LastfmClient(LastAuth auth, HttpClient httpClient = null, ScrobblerBase scrobbler = null) + : base(httpClient) + { + Auth = auth; + _scrobbler = scrobbler; + } + + public LastfmClient(string apiKey, string apiSecret, HttpClient httpClient = null) + : base(httpClient) + { + Auth = new LastAuth(apiKey, apiSecret, httpClient); + } + + public override void Dispose() + { + _albumApi?.Dispose(); + _artistApi?.Dispose(); + _chartApi?.Dispose(); + _libraryApi?.Dispose(); + _scrobbler?.Dispose(); + _tagApi?.Dispose(); + _trackApi?.Dispose(); + _userApi?.Dispose(); + + base.Dispose(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/LibraryApi.cs b/IF.Lastfm.Core/Api/LibraryApi.cs new file mode 100644 index 00000000..f3d98aa7 --- /dev/null +++ b/IF.Lastfm.Core/Api/LibraryApi.cs @@ -0,0 +1,54 @@ +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Commands.Library; +using IF.Lastfm.Core.Helpers; + +namespace IF.Lastfm.Core.Api +{ + public class LibraryApi : ApiBase, ILibraryApi + { + public LibraryApi(ILastAuth auth, HttpClient httpClient = null) + : base(httpClient) + { + Auth = auth; + } + + public Task> GetArtists(string username, DateTimeOffset since, int startIndex = 0, int endIndex = LastFm.DefaultPageLength) + { + throw new NotImplementedException(); + } + + public async Task> GetTracks(string username, string artist, string album, DateTimeOffset since, int pagenumber = 0, int count = LastFm.DefaultPageLength) + { + var command = new GetTracksCommand(Auth, username, artist, album, since) + { + Page = pagenumber, + Count = count, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task RemoveScrobble(string artist, string track, DateTimeOffset timestamp) + { + var command = new RemoveScrobbleCommand(Auth, artist, track, timestamp) + { + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task RemoveTrack(string artist, string track) + { + var command = new RemoveTrackCommand(Auth, artist, track) + { + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/TagApi.cs b/IF.Lastfm.Core/Api/TagApi.cs new file mode 100644 index 00000000..6fcebb74 --- /dev/null +++ b/IF.Lastfm.Core/Api/TagApi.cs @@ -0,0 +1,103 @@ +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Commands.Tag; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Helpers; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Api +{ + public class TagApi : ApiBase, ITagApi + { + + public TagApi(ILastAuth auth, HttpClient httpClient = null) + : base(httpClient) + { + Auth = auth; + } + + /// + /// Search for tags similar to this one. Returns tags ordered by similarity, based on listening data. + /// + public Task> GetSimilarAsync(string tagName) + { + var command = new GetSimilarCommand(Auth, tagName) + { + HttpClient = HttpClient + }; + + return command.ExecuteAsync(); + } + + /// + /// Get the metadata for a tag. + /// + public Task> GetInfoAsync(string tagName) + { + var command = new GetInfoCommand(Auth, tagName) + { + HttpClient = HttpClient + }; + + return command.ExecuteAsync(); + } + + /// + /// Get the top albums tagged by this tag, ordered by tag count. + /// + public Task> GetTopAlbumsAsync(string tagName, int page=1, int itemsPerPage= LastFm.DefaultPageLength) + { + var command = new GetTopAlbumsCommand(Auth, tagName) + { + HttpClient = HttpClient, + Page = page, + Count = itemsPerPage + }; + + return command.ExecuteAsync(); + } + + /// + /// Get the top artists tagged by this tag, ordered by tag count. + /// + public Task> GetTopArtistsAsync(string tagName, int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTopArtistsCommand(Auth, tagName) + { + HttpClient = HttpClient, + Page = page, + Count = itemsPerPage + }; + + return command.ExecuteAsync(); + } + + /// + /// Get the top artists tagged by this tag, ordered by tag count. + /// + public Task> GetTopTracksAsync(string tagName, int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetTopTracksCommand(Auth, tagName) + { + HttpClient = HttpClient, + Page = page, + Count = itemsPerPage + }; + + return command.ExecuteAsync(); + } + + /// + /// Fetches the top global tags on Last.fm, sorted by popularity (number of times used). + /// + public Task> GetTopTagsAsync() + { + var command = new GetTopTagsCommand(Auth) + { + HttpClient = HttpClient + }; + + return command.ExecuteAsync(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/TrackApi.cs b/IF.Lastfm.Core/Api/TrackApi.cs new file mode 100644 index 00000000..3f66dbf6 --- /dev/null +++ b/IF.Lastfm.Core/Api/TrackApi.cs @@ -0,0 +1,118 @@ +using System; +using System.Net.Http; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Commands.Track; +using IF.Lastfm.Core.Helpers; +using IF.Lastfm.Core.Scrobblers; + +namespace IF.Lastfm.Core.Api +{ + public class TrackApi : ApiBase, ITrackApi + { + + public TrackApi(ILastAuth auth, HttpClient httpClient = null) + : base(httpClient) + { + Auth = auth; + } + + [Obsolete("This method has been moved to the Scrobbler class. More information can be found at " + LastFm.SCROBBLING_HELP_URL)] + public Task ScrobbleAsync(Scrobble scrobble) + { + var command = new ScrobbleCommand(Auth, scrobble) + { + HttpClient = HttpClient + }; + return command.ExecuteAsync(); + } + + public Task UpdateNowPlayingAsync(Scrobble scrobble) + { + var command = new UpdateNowPlayingCommand(Auth, scrobble) + { + HttpClient = HttpClient + }; + return command.ExecuteAsync(); + } + + public async Task> GetShoutsForTrackAsync(string trackname, string artistname, bool autocorrect = false, int page = 0, int count = LastFm.DefaultPageLength) + { + var command = new GetShoutsCommand(Auth, trackname, artistname) + { + Page = page, + Count = count, + Autocorrect = autocorrect, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetInfoAsync(string trackname, string artistname, string username = "") + { + var command = new GetInfoCommand(Auth) + { + TrackName = trackname, + ArtistName = artistname, + Username = username, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetInfoByMbidAsync(string mbid) + { + var command = new GetInfoCommand(Auth) + { + TrackMbid = mbid, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetSimilarAsync(string trackname, string artistname, bool autocorrect = false, int limit = 100) + { + var command = new GetSimilarCommand(Auth, trackname, artistname) + { + Autocorrect = autocorrect, + Limit = limit, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task LoveAsync(string trackname, string artistname) + { + var command = new LoveCommand(Auth, trackname, artistname) + { + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task UnloveAsync(string trackname, string artistname) + { + var command = new UnloveCommand(Auth, trackname, artistname) + { + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> SearchAsync(string trackname, string artistname = "", int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new SearchCommand(Auth, trackname, artistname) + { + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Api/UserApi.cs b/IF.Lastfm.Core/Api/UserApi.cs new file mode 100644 index 00000000..9b2a3f10 --- /dev/null +++ b/IF.Lastfm.Core/Api/UserApi.cs @@ -0,0 +1,184 @@ +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Commands.User; +using IF.Lastfm.Core.Helpers; + +namespace IF.Lastfm.Core.Api +{ + public class UserApi : ApiBase, IUserApi + { + + + public UserApi(ILastAuth auth, HttpClient httpClient = null) + : base(httpClient) + { + Auth = auth; + } + + public async Task> GetRecommendedArtistsAsync(int page = 1, int itemsPerPage = LastFm.DefaultPageLength) + { + var command = new GetRecommendedArtistsCommand(Auth) + { + Page = page, + Count = itemsPerPage, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetTopAlbums(string username, LastStatsTimeSpan span, int pagenumber = 0, int count = LastFm.DefaultPageLength) + { + var command = new GetTopAlbumsCommand(Auth, username, span) + { + Page = pagenumber, + Count = count, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetTopArtists(string username, LastStatsTimeSpan span, int pagenumber = 0, int count = LastFm.DefaultPageLength) + { + var command = new GetTopArtistsCommand(Auth, username, span) + { + Page = pagenumber, + Count = count, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + /// + /// Gets a list of recent scrobbled tracks for this user in reverse date order. + /// + /// Username to get scrobbles for. + /// Lower threshold for scrobbles. Will not return scrobbles from before this time. + /// Upper threshold for scrobbles. Will not return scrobbles from after this time. + /// Page numbering starts from 1. If set to 0, will not include the "now playing" track + /// Determines if the response will contain extended data in each artist + /// and whether or not the user has loved each track + /// Amount of scrobbles to return for this page. + /// Enumerable of LastTrack + public async Task> GetRecentScrobbles(string username, DateTimeOffset? from = null, + DateTimeOffset? to = null, bool extendedResponse = false, int pagenumber = LastFm.DefaultPage, + int count = LastFm.DefaultPageLength) + { + var command = new GetRecentTracksCommand(Auth, username) + { + Page = pagenumber, + Count = count, + From = from, + To = to, + Extended = extendedResponse, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetRecentStations(string username, int pagenumber = 0, int count = LastFm.DefaultPageLength) + { + var command = new GetRecentStationsCommand(Auth, username) + { + Page = pagenumber, + Count = count, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetShoutsAsync(string username, int pagenumber, int count = LastFm.DefaultPageLength) + { + var command = new GetShoutsCommand(Auth, username) + { + Page = pagenumber, + Count = count, + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetInfoAsync(string username) + { + var command = new GetInfoCommand(Auth, username) + { + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task AddShoutAsync(string recipient, string message) + { + var command = new AddShoutCommand(Auth, recipient, message) + { + HttpClient = HttpClient + }; + + return await command.ExecuteAsync(); + } + + public async Task> GetLovedTracks( + string username, + int pagenumber = 1, + int count = LastFm.DefaultPageLength) + { + var command = new GetLovedTracksCommand(auth: Auth, username: username) + { + Page = pagenumber, + Count = count, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + public async Task> GetWeeklyChartListAsync(string username) + { + var command = new GetWeeklyChartListCommand(auth: Auth, username: username) + { + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetWeeklyArtistChartAsync(string username, double? from = null, double? to = null) + { + var command = new GetWeeklyArtistChartCommand(auth: Auth, username: username) + { + From = from, + To = to, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetWeeklyTrackChartAsync(string username, double? from = null, double? to = null) + { + var command = new GetWeeklyTrackChartCommand(auth: Auth, username: username) + { + From = from, + To = to, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + + public async Task> GetWeeklyAlbumChartAsync(string username, double? from = null, double? to = null) + { + var command = new GetWeeklyAlbumChartCommand(auth: Auth, username: username) + { + From = from, + To = to, + HttpClient = HttpClient + }; + return await command.ExecuteAsync(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Helpers/ApiBase.cs b/IF.Lastfm.Core/Helpers/ApiBase.cs new file mode 100644 index 00000000..dab683ad --- /dev/null +++ b/IF.Lastfm.Core/Helpers/ApiBase.cs @@ -0,0 +1,41 @@ +using IF.Lastfm.Core.Api; +using System; +using System.Net.Http; + +namespace IF.Lastfm.Core.Helpers +{ + public abstract class ApiBase : IDisposable + { + private readonly bool _isHttpClientOwner; + + public ILastAuth Auth { get; protected set; } + + /// + /// The HttpClient that will be used by this API. If it is provided through the ApiBase constructor then it should be disposed explicitly. + /// + public HttpClient HttpClient { get; } + + protected ApiBase(HttpClient httpClient = null) + { + if (httpClient == null) + { + httpClient = new HttpClient(); + + // See http://stackoverflow.com/questions/14595021/how-to-disable-the-expect-100-continue-header-in-winrts-httpwebrequest + httpClient.DefaultRequestHeaders.ExpectContinue = false; + + _isHttpClientOwner = true; + } + + HttpClient = httpClient; + } + + public virtual void Dispose() + { + if (_isHttpClientOwner) + { + HttpClient.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Helpers/CountingHttpClientHandler.cs b/IF.Lastfm.Core/Helpers/CountingHttpClientHandler.cs new file mode 100644 index 00000000..6f098230 --- /dev/null +++ b/IF.Lastfm.Core/Helpers/CountingHttpClientHandler.cs @@ -0,0 +1,17 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace IF.Lastfm.Core.Helpers +{ + public class CountingHttpClientHandler : HttpClientHandler + { + public int Count { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Count++; + return base.SendAsync(request, cancellationToken); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Helpers/EnumerableExtensions.cs b/IF.Lastfm.Core/Helpers/EnumerableExtensions.cs new file mode 100644 index 00000000..6269a03b --- /dev/null +++ b/IF.Lastfm.Core/Helpers/EnumerableExtensions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; + +namespace IF.Lastfm.Core.Helpers +{ + public static class EnumerableExtensions + { + /// + /// http://stackoverflow.com/a/419063 + /// + public static IEnumerable> Batch(this IEnumerable source, int max) + { + List toReturn = new List(max); + foreach (var item in source) + { + toReturn.Add(item); + if (toReturn.Count == max) + { + yield return toReturn; + toReturn = new List(max); + } + } + if (toReturn.Any()) + { + yield return toReturn; + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Helpers/FakeResponseHandler.cs b/IF.Lastfm.Core/Helpers/FakeResponseHandler.cs new file mode 100644 index 00000000..7640638d --- /dev/null +++ b/IF.Lastfm.Core/Helpers/FakeResponseHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace IF.Lastfm.Core.Helpers +{ + /// + /// http://stackoverflow.com/a/22264503/268555 + /// + public class FakeResponseHandler : DelegatingHandler + { + private readonly Dictionary _fakeResponses = new Dictionary(); + + public void AddFakeResponse(Uri uri, HttpResponseMessage responseMessage) + { + _fakeResponses.Add(uri, responseMessage); + } + + protected override Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) + { + HttpResponseMessage response; + if (_fakeResponses.ContainsKey(request.RequestUri)) + { + response = _fakeResponses[request.RequestUri]; + } + else + { + response = new HttpResponseMessage(HttpStatusCode.NotFound) { RequestMessage = request }; + } + + return Task.FromResult(response); + } + } +} diff --git a/IF.Lastfm.Core/Helpers/QueueFakeResponseHandler.cs b/IF.Lastfm.Core/Helpers/QueueFakeResponseHandler.cs new file mode 100644 index 00000000..02636d4c --- /dev/null +++ b/IF.Lastfm.Core/Helpers/QueueFakeResponseHandler.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace IF.Lastfm.Core.Helpers +{ + public class QueueFakeResponseHandler : DelegatingHandler + { + private readonly Queue _queuedResponses; + + public List> Sent { get; private set; } + + public QueueFakeResponseHandler() + { + _queuedResponses = new Queue(); + Sent = new List>(); + } + + public void Enqueue(HttpResponseMessage message) + { + _queuedResponses.Enqueue(message); + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + Sent.Add(Tuple.Create(request, await request.Content.ReadAsStringAsync())); + var response = _queuedResponses.Dequeue() + ?? new HttpResponseMessage(HttpStatusCode.NotFound) {RequestMessage = request}; + + return response; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/IF.Lastfm.Core.csproj b/IF.Lastfm.Core/IF.Lastfm.Core.csproj new file mode 100644 index 00000000..29d5c689 --- /dev/null +++ b/IF.Lastfm.Core/IF.Lastfm.Core.csproj @@ -0,0 +1,18 @@ + + + + + + + + + netstandard1.1 + true + + + + + + + + \ No newline at end of file diff --git a/IF.Lastfm.Core/Json/LastFmBooleanConverter.cs b/IF.Lastfm.Core/Json/LastFmBooleanConverter.cs new file mode 100644 index 00000000..ec2640a2 --- /dev/null +++ b/IF.Lastfm.Core/Json/LastFmBooleanConverter.cs @@ -0,0 +1,25 @@ +using System; +using Newtonsoft.Json; + +namespace IF.Lastfm.Core.Json +{ + public class LastFmBooleanConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + serializer.Serialize(writer, value); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = reader.Value.ToString(); + + return jo == "1"; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(string); + } + } +} diff --git a/IF.Lastfm.Core/Json/PageResponseJsonConverter.cs b/IF.Lastfm.Core/Json/PageResponseJsonConverter.cs new file mode 100644 index 00000000..396c4019 --- /dev/null +++ b/IF.Lastfm.Core/Json/PageResponseJsonConverter.cs @@ -0,0 +1,39 @@ +using System; +using IF.Lastfm.Core.Api.Helpers; +using Newtonsoft.Json; + +namespace IF.Lastfm.Core.Json +{ + public class PageResponseJsonConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var pageResponse = (IPageResponse) value; + + dynamic container = new + { + success = pageResponse.Success, + items = pageResponse.Content, + page = new + { + totalItems = pageResponse.TotalItems, + pageSize = pageResponse.PageSize, + page = pageResponse.Page, + totalPages = pageResponse.TotalPages + } + }; + + serializer.Serialize(writer, container); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return serializer.Deserialize(reader, objectType); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof (PageResponse<>); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/LastFm.cs b/IF.Lastfm.Core/LastFm.cs new file mode 100644 index 00000000..5a2e7e6e --- /dev/null +++ b/IF.Lastfm.Core/LastFm.cs @@ -0,0 +1,126 @@ +using IF.Lastfm.Core.Api.Enums; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Text; + +[assembly: InternalsVisibleTo("IF.Lastfm.Core.Tests")] +[assembly: InternalsVisibleTo("IF.Lastfm.Core.Tests.Integration")] +[assembly: InternalsVisibleTo("IF.Lastfm.SQLite")] +[assembly: InternalsVisibleTo("IF.Lastfm.Syro")] +namespace IF.Lastfm.Core +{ + public static class LastFm + { + internal const string SCROBBLING_HELP_URL = "https://github.com/inflatablefriends/lastfm/blob/scrobbler/doc/scrobbling.md"; + internal const string TEST_APIKEY = "a6ab4b9376e54cdb06912bfbd9c1f288"; + internal const string TEST_APISECRET = "3aa7202fd1bc6d5a7ac733246cbccc4b"; + + public const string ApiRoot = "http://ws.audioscrobbler.com/2.0/"; + public const string ApiRootSsl = "https://ws.audioscrobbler.com/2.0/"; + private const string ApiRootFormat = "{0}://ws.audioscrobbler.com/2.0/?method={1}&api_key={2}{3}"; + + private const string ResponseFormat = "json"; + + public const string DefaultLanguageCode = "en"; + + public const int DefaultPageLength = 20; + public const int DefaultPage = 1; + + public static string FormatApiUrl(string method, string apikey, Dictionary parameters = null, bool secure = false) + { + if (parameters == null) + { + parameters = new Dictionary(); + } + + parameters.Add("format", ResponseFormat); + + var querystring = LastFm.FormatQueryParameters(parameters.OrderBy(kv => kv.Key)); + + var protocol = secure + ? "https" + : "http"; + + return string.Format(ApiRootFormat, protocol, method, apikey, querystring); + } + + public static FormUrlEncodedContent CreatePostBody(string method, string apikey, string apisig, + IEnumerable> parameters) + { + var init = new Dictionary + { + {"method", method}, + {"api_key", apikey}, + {"api_sig", apisig}, + {"format", ResponseFormat} + }; + + // TODO ordering + var requestParameters = init.Concat(parameters); + + return new FormUrlEncodedContent(requestParameters); + } + + public static string FormatQueryParameters(IEnumerable> parameters) + { + const string parameterFormat = "&{0}={1}"; + + var builder = new StringBuilder(); + + foreach (var pair in parameters) + { + builder.Append(string.Format(parameterFormat, pair.Key, pair.Value)); + } + + return builder.ToString(); + } + + /// + /// TODO see issue #5 + /// + /// String of JSON + /// Enum indicating the Status, Unknown if there is no Status + /// True when the JSON could be parsed and it didn't describe a known Last.Fm Status. + public static bool IsResponseValid(string json, out LastResponseStatus status) + { + if (string.IsNullOrWhiteSpace(json)) + { + status = LastResponseStatus.Unknown; + return false; + } + + JObject jo; + try + { + jo = JsonConvert.DeserializeObject(json); + } + catch (JsonException) + { + status = LastResponseStatus.Unknown; + return false; + } + + var codeString = jo.Value("error"); + if (string.IsNullOrWhiteSpace(codeString) && json.Length > 1) + { + status = LastResponseStatus.Successful; + return true; + } + + status = LastResponseStatus.Unknown; + + int code; + if (Int32.TryParse(codeString, out code)) + { + status = (LastResponseStatus) code; + } + + return false; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/MD5.cs b/IF.Lastfm.Core/MD5.cs new file mode 100644 index 00000000..0e437f1a --- /dev/null +++ b/IF.Lastfm.Core/MD5.cs @@ -0,0 +1,295 @@ +//// ************************************************************** +//// * Raw implementation of the MD5 hash algorithm +//// * from RFC 1321. +//// * +//// * Written By: Reid Borsuk and Jenny Zheng +//// * Copyright (c) Microsoft Corporation. All rights reserved. +//// ************************************************************** + +#region + +using System; +using System.Text; + +#endregion + +namespace IF.Lastfm.Core +{ + /// + /// Simple struct for the (a,b,c,d) which is used to compute the mesage digest. + /// + internal struct ABCDStruct + { + public uint A; + public uint B; + public uint C; + public uint D; + } + + /// + /// Raw implementation of the MD5 hash algorithm rom RFC 1321. + /// + public sealed class MD5 + { + //// Prevent CSC from adding a default public constructor + private MD5() + { + } + + public static byte[] GetHash(string input, Encoding encoding) + { + if (null == input) + { + throw new ArgumentNullException("input", "Unable to calculate hash over null input data"); + } + if (null == encoding) + { + throw new ArgumentNullException("encoding", + "Unable to calculate hash over a string without a default encoding. Consider using the GetHash(string) overload to use UTF8 Encoding"); + } + var target = encoding.GetBytes(input); + return GetHash(target); + } + + public static byte[] GetHash(string input) + { + return GetHash(input, new UTF8Encoding()); + } + + public static string GetHashString(byte[] input) + { + if (null == input) + { + throw new ArgumentNullException("input", "Unable to calculate hash over null input data"); + } + var retval = BitConverter.ToString(GetHash(input)); + retval = retval.Replace("-", string.Empty); + return retval; + } + + public static string GetHashString(string input, Encoding encoding) + { + if (null == input) + { + throw new ArgumentNullException("input", "Unable to calculate hash over null input data"); + } + if (null == encoding) + { + throw new ArgumentNullException("encoding", + "Unable to calculate hash over a string without a default encoding. Consider using the GetHashString(string) overload to use UTF8 Encoding"); + } + var target = encoding.GetBytes(input); + return GetHashString(target); + } + + public static string GetHashString(string input) + { + return GetHashString(input, new UTF8Encoding()); + } + + public static byte[] GetHash(byte[] input) + { + if (null == input) + { + throw new ArgumentNullException("input", "Unable to calculate hash over null input data"); + } + + //// Intitial values defined in RFC 1321 + var abcd = new ABCDStruct(); + abcd.A = 0x67452301; + abcd.B = 0xefcdab89; + abcd.C = 0x98badcfe; + abcd.D = 0x10325476; + + //// We pass in the input array by block, the final block of data must be handled specialy for padding & length embeding + var startIndex = 0; + while (startIndex <= input.Length - 64) + { + GetHashBlock(input, ref abcd, startIndex); + startIndex += 64; + } + //// The final data block. + return GetHashFinalBlock(input, startIndex, input.Length - startIndex, abcd, (Int64) input.Length*8); + } + + internal static byte[] GetHashFinalBlock(byte[] input, int ibStart, int cbSize, ABCDStruct ABCD, Int64 len) + { + var working = new byte[64]; + var length = BitConverter.GetBytes(len); + + //// Padding is a single bit 1, followed by the number of 0s required to make size congruent to 448 modulo 512. Step 1 of RFC 1321 + //// The CLR ensures that our buffer is 0-assigned, we don't need to explicitly set it. This is why it ends up being quicker to just + //// use a temporary array rather then doing in-place assignment (5% for small inputs) + Array.Copy(input, ibStart, working, 0, cbSize); + working[cbSize] = 0x80; + + //// We have enough room to store the length in this chunk + if (cbSize < 56) + { + Array.Copy(length, 0, working, 56, 8); + GetHashBlock(working, ref ABCD, 0); + } + else //// We need an aditional chunk to store the length + { + GetHashBlock(working, ref ABCD, 0); + //// Create an entirely new chunk due to the 0-assigned trick mentioned above, to avoid an extra function call clearing the array + working = new byte[64]; + Array.Copy(length, 0, working, 56, 8); + GetHashBlock(working, ref ABCD, 0); + } + var output = new byte[16]; + Array.Copy(BitConverter.GetBytes(ABCD.A), 0, output, 0, 4); + Array.Copy(BitConverter.GetBytes(ABCD.B), 0, output, 4, 4); + Array.Copy(BitConverter.GetBytes(ABCD.C), 0, output, 8, 4); + Array.Copy(BitConverter.GetBytes(ABCD.D), 0, output, 12, 4); + return output; + } + + //// Performs a single block transform of MD5 for a given set of ABCD inputs + /* If implementing your own hashing framework, be sure to set the initial ABCD correctly according to RFC 1321: + // A = 0x67452301; + // B = 0xefcdab89; + // C = 0x98badcfe; + // D = 0x10325476; + */ + + internal static void GetHashBlock(byte[] input, ref ABCDStruct ABCDValue, int ibStart) + { + var temp = Converter(input, ibStart); + var a = ABCDValue.A; + var b = ABCDValue.B; + var c = ABCDValue.C; + var d = ABCDValue.D; + + a = r1(a, b, c, d, temp[0], 7, 0xd76aa478); + d = r1(d, a, b, c, temp[1], 12, 0xe8c7b756); + c = r1(c, d, a, b, temp[2], 17, 0x242070db); + b = r1(b, c, d, a, temp[3], 22, 0xc1bdceee); + a = r1(a, b, c, d, temp[4], 7, 0xf57c0faf); + d = r1(d, a, b, c, temp[5], 12, 0x4787c62a); + c = r1(c, d, a, b, temp[6], 17, 0xa8304613); + b = r1(b, c, d, a, temp[7], 22, 0xfd469501); + a = r1(a, b, c, d, temp[8], 7, 0x698098d8); + d = r1(d, a, b, c, temp[9], 12, 0x8b44f7af); + c = r1(c, d, a, b, temp[10], 17, 0xffff5bb1); + b = r1(b, c, d, a, temp[11], 22, 0x895cd7be); + a = r1(a, b, c, d, temp[12], 7, 0x6b901122); + d = r1(d, a, b, c, temp[13], 12, 0xfd987193); + c = r1(c, d, a, b, temp[14], 17, 0xa679438e); + b = r1(b, c, d, a, temp[15], 22, 0x49b40821); + + a = r2(a, b, c, d, temp[1], 5, 0xf61e2562); + d = r2(d, a, b, c, temp[6], 9, 0xc040b340); + c = r2(c, d, a, b, temp[11], 14, 0x265e5a51); + b = r2(b, c, d, a, temp[0], 20, 0xe9b6c7aa); + a = r2(a, b, c, d, temp[5], 5, 0xd62f105d); + d = r2(d, a, b, c, temp[10], 9, 0x02441453); + c = r2(c, d, a, b, temp[15], 14, 0xd8a1e681); + b = r2(b, c, d, a, temp[4], 20, 0xe7d3fbc8); + a = r2(a, b, c, d, temp[9], 5, 0x21e1cde6); + d = r2(d, a, b, c, temp[14], 9, 0xc33707d6); + c = r2(c, d, a, b, temp[3], 14, 0xf4d50d87); + b = r2(b, c, d, a, temp[8], 20, 0x455a14ed); + a = r2(a, b, c, d, temp[13], 5, 0xa9e3e905); + d = r2(d, a, b, c, temp[2], 9, 0xfcefa3f8); + c = r2(c, d, a, b, temp[7], 14, 0x676f02d9); + b = r2(b, c, d, a, temp[12], 20, 0x8d2a4c8a); + + a = r3(a, b, c, d, temp[5], 4, 0xfffa3942); + d = r3(d, a, b, c, temp[8], 11, 0x8771f681); + c = r3(c, d, a, b, temp[11], 16, 0x6d9d6122); + b = r3(b, c, d, a, temp[14], 23, 0xfde5380c); + a = r3(a, b, c, d, temp[1], 4, 0xa4beea44); + d = r3(d, a, b, c, temp[4], 11, 0x4bdecfa9); + c = r3(c, d, a, b, temp[7], 16, 0xf6bb4b60); + b = r3(b, c, d, a, temp[10], 23, 0xbebfbc70); + a = r3(a, b, c, d, temp[13], 4, 0x289b7ec6); + d = r3(d, a, b, c, temp[0], 11, 0xeaa127fa); + c = r3(c, d, a, b, temp[3], 16, 0xd4ef3085); + b = r3(b, c, d, a, temp[6], 23, 0x04881d05); + a = r3(a, b, c, d, temp[9], 4, 0xd9d4d039); + d = r3(d, a, b, c, temp[12], 11, 0xe6db99e5); + c = r3(c, d, a, b, temp[15], 16, 0x1fa27cf8); + b = r3(b, c, d, a, temp[2], 23, 0xc4ac5665); + + a = r4(a, b, c, d, temp[0], 6, 0xf4292244); + d = r4(d, a, b, c, temp[7], 10, 0x432aff97); + c = r4(c, d, a, b, temp[14], 15, 0xab9423a7); + b = r4(b, c, d, a, temp[5], 21, 0xfc93a039); + a = r4(a, b, c, d, temp[12], 6, 0x655b59c3); + d = r4(d, a, b, c, temp[3], 10, 0x8f0ccc92); + c = r4(c, d, a, b, temp[10], 15, 0xffeff47d); + b = r4(b, c, d, a, temp[1], 21, 0x85845dd1); + a = r4(a, b, c, d, temp[8], 6, 0x6fa87e4f); + d = r4(d, a, b, c, temp[15], 10, 0xfe2ce6e0); + c = r4(c, d, a, b, temp[6], 15, 0xa3014314); + b = r4(b, c, d, a, temp[13], 21, 0x4e0811a1); + a = r4(a, b, c, d, temp[4], 6, 0xf7537e82); + d = r4(d, a, b, c, temp[11], 10, 0xbd3af235); + c = r4(c, d, a, b, temp[2], 15, 0x2ad7d2bb); + b = r4(b, c, d, a, temp[9], 21, 0xeb86d391); + + ABCDValue.A = unchecked(a + ABCDValue.A); + ABCDValue.B = unchecked(b + ABCDValue.B); + ABCDValue.C = unchecked(c + ABCDValue.C); + ABCDValue.D = unchecked(d + ABCDValue.D); + } + + //// Manually unrolling these equations nets us a 20% performance improvement + private static uint r1(uint a, uint b, uint c, uint d, uint x, int s, uint t) + { + //// (b + LSR((a + F(b, c, d) + x + t), s)) + //// F(x, y, z) ((x & y) | ((x ^ 0xFFFFFFFF) & z)) + return unchecked(b + LSR((a + ((b & c) | ((b ^ 0xFFFFFFFF) & d)) + x + t), s)); + } + + private static uint r2(uint a, uint b, uint c, uint d, uint x, int s, uint t) + { + //// (b + LSR((a + G(b, c, d) + x + t), s)) + //// G(x, y, z) ((x & z) | (y & (z ^ 0xFFFFFFFF))) + return unchecked(b + LSR((a + ((b & d) | (c & (d ^ 0xFFFFFFFF))) + x + t), s)); + } + + private static uint r3(uint a, uint b, uint c, uint d, uint x, int s, uint t) + { + //// (b + LSR((a + H(b, c, d) + k + i), s)) + //// H(x, y, z) (x ^ y ^ z) + return unchecked(b + LSR((a + (b ^ c ^ d) + x + t), s)); + } + + private static uint r4(uint a, uint b, uint c, uint d, uint x, int s, uint t) + { + //// (b + LSR((a + I(b, c, d) + k + i), s)) + //// I(x, y, z) (y ^ (x | (z ^ 0xFFFFFFFF))) + return unchecked(b + LSR((a + (c ^ (b | (d ^ 0xFFFFFFFF))) + x + t), s)); + } + + //// Implementation of left rotate + //// s is an int instead of a uint becuase the CLR requires the argument passed to >>/<< is of + //// type int. Doing the demoting inside this function would add overhead. + private static uint LSR(uint i, int s) + { + return ((i << s) | (i >> (32 - s))); + } + + //// Convert input array into array of UInts + private static uint[] Converter(byte[] input, int ibStart) + { + if (null == input) + { + throw new ArgumentNullException("input", "Unable convert null array to array of uInts"); + } + + var result = new uint[16]; + for (var i = 0; i < 16; i++) + { + result[i] = input[ibStart + i*4]; + result[i] += (uint) input[ibStart + i*4 + 1] << 8; + result[i] += (uint) input[ibStart + i*4 + 2] << 16; + result[i] += (uint) input[ibStart + i*4 + 3] << 24; + } + + return result; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/BuyLink.cs b/IF.Lastfm.Core/Objects/BuyLink.cs new file mode 100644 index 00000000..71f42354 --- /dev/null +++ b/IF.Lastfm.Core/Objects/BuyLink.cs @@ -0,0 +1,6 @@ +namespace IF.Lastfm.Core.Objects +{ + public class BuyLink + { + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/CountryCode.cs b/IF.Lastfm.Core/Objects/CountryCode.cs new file mode 100644 index 00000000..e2fc8d49 --- /dev/null +++ b/IF.Lastfm.Core/Objects/CountryCode.cs @@ -0,0 +1,6 @@ +namespace IF.Lastfm.Core.Objects +{ + public enum CountryCode + { + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/ILastFmObject.cs b/IF.Lastfm.Core/Objects/ILastFmObject.cs new file mode 100644 index 00000000..966548bb --- /dev/null +++ b/IF.Lastfm.Core/Objects/ILastFmObject.cs @@ -0,0 +1,7 @@ +namespace IF.Lastfm.Core.Objects +{ + internal interface ILastfmObject + { + + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastAlbum.cs b/IF.Lastfm.Core/Objects/LastAlbum.cs new file mode 100644 index 00000000..13df141c --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastAlbum.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Objects +{ + public class LastAlbum : ILastfmObject + { + public string Id { get; set; } + + public string Name { get; set; } + + public IEnumerable Tracks { get; set; } + + public string ArtistName { get; set; } + + public string ArtistMbid { get; set; } + + public DateTimeOffset? ReleaseDateUtc { get; set; } + + public int? ListenerCount { get; set; } + + public int? PlayCount { get; set; } + + public int? UserPlayCount { get; set; } + + public string Mbid { get; set; } + + public IEnumerable TopTags { get; set; } + + public Uri Url { get; set; } + + public LastImageSet Images { get; set; } + + internal static LastAlbum ParseJToken(JToken token) + { + var a = new LastAlbum(); + + a.Id = token.Value("id"); + var artistToken = token["artist"]; + switch (artistToken.Type) + { + case JTokenType.String: + a.ArtistName = token.Value("artist"); + break; + case JTokenType.Object: + if(!string.IsNullOrEmpty(artistToken.Value("name"))) + { + a.ArtistName = artistToken.Value("name"); + } + else if(!string.IsNullOrEmpty(artistToken.Value("#text"))) + { + //in user.getWeeklyAlbumChart artist name is returned with key #text + a.ArtistName = artistToken.Value("#text"); + } + if(!string.IsNullOrEmpty(artistToken.Value("mbid"))) + { + a.ArtistMbid = artistToken.Value("mbid"); + } + break; + + } + + var tracksToken = token.SelectToken("tracks"); + if (tracksToken != null) + { + var trackToken = tracksToken.SelectToken("track"); + if (trackToken != null) + a.Tracks = trackToken.Type == JTokenType.Array + ? trackToken.Children().Select(t => LastTrack.ParseJToken(t, a.Name)) + : new List() {LastTrack.ParseJToken(trackToken, a.Name)}; + } + else + { + a.Tracks = Enumerable.Empty(); + } + + var tagsToken = token.SelectToken("toptags"); + if (tagsToken != null) + { + var tagToken = tagsToken.SelectToken("tag"); + if (tagToken != null) + { + a.TopTags = + tagToken.Type == JTokenType.Array + ? tagToken.Children().Select(token1 => LastTag.ParseJToken(token1)) + : new List { LastTag.ParseJToken(tagToken) }; + } + } + else + { + a.TopTags = Enumerable.Empty(); + } + + a.ListenerCount = token.Value("listeners"); + a.Mbid = token.Value("mbid"); + a.Name = token.Value("name"); + + var playCountStr = token.Value("playcount"); + int playCount; + if (int.TryParse(playCountStr, out playCount)) + { + a.PlayCount = playCount; + } + + var userPlayCountStr = token.Value("userplaycount"); + int userPlayCount; + if (int.TryParse(userPlayCountStr, out userPlayCount)) + { + a.UserPlayCount = userPlayCount; + } + + var images = token.SelectToken("image"); + if (images != null) + { + var imageCollection = LastImageSet.ParseJToken(images); + a.Images = imageCollection; + } + + a.Url = new Uri(token.Value("url"), UriKind.Absolute); + + var dateString = token.Value("releasedate"); + DateTimeOffset releaseDate; + if (DateTimeOffset.TryParse(dateString, out releaseDate)) + { + a.ReleaseDateUtc = releaseDate; + } + + return a; + } + + internal static string GetNameFromJToken(JToken albumToken) + { + var name = albumToken.Value("title") + ?? albumToken.Value("#text") + ?? albumToken.Value("name"); // Used in Library track lists + + return name; + } + } +} diff --git a/IF.Lastfm.Core/Objects/LastArtist.cs b/IF.Lastfm.Core/Objects/LastArtist.cs new file mode 100644 index 00000000..074982cc --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastArtist.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Objects +{ + /// + /// TODO streamable + /// "streamable": "0" + /// + /// TODO band members + /// "bandmembers": { + /// "member": [ + /// { + /// "name": "Scott Hutchison", + /// "yearfrom": "2003" + /// }, + /// { + /// "name": "Billy Kennedy", + /// "yearfrom": "2006" + /// }, + /// { + /// "name": "Grant Hutchison", + /// "yearfrom": "2004" + /// }, + /// { + /// "name": "Andy Monaghan", + /// "yearfrom": "2008" + /// }, + /// { + /// "name": "Gordon Skene", + /// "yearfrom": "2009" + /// } + /// ] + /// } + /// + /// TODO context -> similar, rename similar to related + /// + public class LastArtist : ILastfmObject + { + #region Properties + + public string Id { get; set; } + public string Name { get; set; } + public LastWiki Bio { get; set; } + public string Mbid { get; set; } + public Uri Url { get; set; } + public bool OnTour { get; set; } + public IEnumerable Tags { get; set; } + public List Similar { get; set; } + public LastImageSet MainImage { get; set; } + public int? PlayCount { get; set; } + public LastStats Stats { get; set; } + + #endregion + + public LastArtist() + { + Tags = Enumerable.Empty(); + Similar = Enumerable.Empty().ToList(); + } + + internal static LastArtist ParseJToken(JToken token) + { + var a = new LastArtist(); + + a.Id = token.Value("id"); + a.Name = token.Value("name"); + a.Mbid = token.Value("mbid"); + var url = token.Value("url"); + + var playCountStr = token.Value("playcount"); + int playCount; + if (int.TryParse(playCountStr, out playCount)) + { + a.PlayCount = playCount; + } + + // for some stupid reason the api returns the url without http in the get similar method, WHY? + if (!url.StartsWith("http")) + url = "http://" + url; + + a.Url = new Uri(url, UriKind.Absolute); + + a.OnTour = Convert.ToBoolean(token.Value("ontour")); + + var statsToken = token.SelectToken("stats"); + if (statsToken != null) + { + a.Stats = LastStats.ParseJToken(statsToken); + } + + var bioToken = token.SelectToken("bio"); + if (bioToken != null) + { + a.Bio = LastWiki.ParseJToken(bioToken); + } + + var tagsToken = token.SelectToken("tags"); + if (tagsToken != null) + { + var tagToken = tagsToken.SelectToken("tag"); + if (tagToken != null) + { + a.Tags = + tagToken.Type == JTokenType.Array + ? tagToken.Children().Select(token1 => LastTag.ParseJToken(token1)) + : new List { LastTag.ParseJToken(tagToken) }; + } + } + + var images = token.SelectToken("image"); + if (images != null && images.HasValues) + { + var imageCollection = LastImageSet.ParseJToken(images); + a.MainImage = imageCollection; + } + + var similarToken = token.SelectToken("similar"); + if (similarToken != null) + { + a.Similar = new List(); + var similarArtists = similarToken.SelectToken("artist"); + if (similarArtists != null && similarArtists.Children().Any()) + { + // array notation isn't used on the api when only one object is available + if (similarArtists.Type != JTokenType.Array) + { + var item = ParseJToken(similarArtists); + a.Similar.Add(item); + } + else + { + var items = similarArtists.Children().Select(ParseJToken); + a.Similar.AddRange(items); + } + } + } + + return a; + } + + internal static string GetNameFromJToken(JToken artistToken) + { + var name = artistToken.Value("name"); + + if (string.IsNullOrEmpty(name)) + { + name = artistToken.Value("#text"); + } + + return name; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastImageSet.cs b/IF.Lastfm.Core/Objects/LastImageSet.cs new file mode 100644 index 00000000..3b467828 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastImageSet.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Objects +{ + public class LastImageSet : IEnumerable + { + public LastImageSet() + { + } + + public LastImageSet(string s, string m, string l, string xl, string xxl = null) + { + Small = new Uri(s); + Medium = new Uri(m); + Large = new Uri(l); + ExtraLarge = new Uri(xl); + + if (xxl != null) + { + Mega = new Uri(xxl); + } + } + + public Uri Small { get; set; } + + public Uri Medium { get; set; } + + public Uri Large { get; set; } + + public Uri ExtraLarge { get; set; } + + public Uri Mega { get; set; } + + public Uri Largest => Mega ?? ExtraLarge ?? Large ?? Medium ?? Small; + + private IEnumerable Images => new [] {Small, Medium, Large, ExtraLarge, Mega}.Where(uri => uri != null); + + public static LastImageSet ParseJToken(JToken images) + { + var c = new LastImageSet(); + + foreach (var image in images.Children()) + { + var size = image.Value("size"); + var uriString = image.Value("#text"); + + if (string.IsNullOrEmpty(uriString)) + { + break; + } + + var uri = new Uri(uriString, UriKind.Absolute); + + switch (size) + { + case "small": + c.Small = uri; + break; + case "medium": + c.Medium = uri; + break; + case "large": + c.Large = uri; + break; + case "extralarge": + c.ExtraLarge = uri; + break; + case "mega": + c.Mega = uri; + break; + } + } + + return c; + } + + public IEnumerator GetEnumerator() + { + return Images.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastShout.cs b/IF.Lastfm.Core/Objects/LastShout.cs new file mode 100644 index 00000000..73393eb8 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastShout.cs @@ -0,0 +1,46 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace IF.Lastfm.Core.Objects +{ + public class LastShout : ILastfmObject + { + #region Properties + + public string Body { get; set; } + + public string Author { get; set; } + + public DateTimeOffset TimePosted { get; set; } + + #endregion + + public LastShout() + { + } + + public LastShout(string author, string body, string time) + { + Author = author; + Body = body; + TimePosted = DateTime.Parse(time); + } + + public static LastShout ParseJToken(JToken token) + { + var s = new LastShout(); + + s.Body = token.Value("body"); + s.Author = token.Value("author"); + + var date = token.Value("date"); + DateTimeOffset postedAt; + if (DateTimeOffset.TryParse(date, out postedAt)) + { + s.TimePosted = postedAt; + } + + return s; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastStation.cs b/IF.Lastfm.Core/Objects/LastStation.cs new file mode 100644 index 00000000..b28aa243 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastStation.cs @@ -0,0 +1,21 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Objects +{ + public class LastStation : ILastfmObject + { + public string Name { get; set; } + public Uri Url { get; set; } + + internal static LastStation ParseJToken(JToken token) + { + var s = new LastStation(); + + s.Name = token.Value("name"); + s.Url = new Uri(token.Value("url"), UriKind.Absolute); + + return s; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastStats.cs b/IF.Lastfm.Core/Objects/LastStats.cs new file mode 100644 index 00000000..0aaae737 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastStats.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Objects +{ + public class LastStats : ILastfmObject + { + #region Properties + + public int Listeners { get; set; } + public int Plays { get; set; } + + #endregion + + internal static LastStats ParseJToken(JToken token) + { + var stats = new LastStats + { + Listeners = token.Value("listeners"), + Plays = token.Value("plays") + }; + + return stats; + } + } +} diff --git a/IF.Lastfm.Core/Objects/LastTag.cs b/IF.Lastfm.Core/Objects/LastTag.cs new file mode 100644 index 00000000..a9ebc233 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastTag.cs @@ -0,0 +1,77 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Objects +{ + public class LastTag : ILastfmObject + { + #region Properties + + public string Name { get; set; } + + public Uri Url { get; set; } + + public int? Count { get; set; } + + public string RelatedTo { get; set; } + + public bool? Streamable { get; set; } + + /// + /// The number of users that have used this tag + /// + public int? Reach { get; set; } + + #endregion + + public LastTag() + { + } + + public LastTag(string name, string uri, int? count = null) + { + Name = name; + Url = new Uri(uri, UriKind.RelativeOrAbsolute); + Count = count; + } + + internal static LastTag ParseJToken(JToken token) + { + return ParseJToken(token, null); + } + + internal static LastTag ParseJToken(JToken token, string relatedTag) + { + var name = token.Value("name"); + var url = token.Value("url"); + + int? count = null; + var countToken = token.SelectToken("count") ?? token.SelectToken("taggings"); + if (countToken != null) + { + count = countToken.ToObject(); + } + + bool? streamable = null; + var streamableToken = token.SelectToken("streamable"); + if (streamableToken != null) + { + streamable = Convert.ToBoolean(streamableToken.Value()); + } + + int? reach = null; + var reachToken = token.SelectToken("reach"); + if (reachToken != null) + { + reach = reachToken.ToObject(); + } + + return new LastTag(name, url, count) + { + Streamable = streamable, + RelatedTo = relatedTag, + Reach = reach + }; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastTrack.cs b/IF.Lastfm.Core/Objects/LastTrack.cs new file mode 100644 index 00000000..c2ec80f4 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastTrack.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using IF.Lastfm.Core.Api.Helpers; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Objects +{ + /// + /// TODO Wiki, Stream availability + /// + public class LastTrack : ILastfmObject + { + #region Properties + + public string Id { get; set; } + + public string Name { get; set; } + + public TimeSpan? Duration { get; set; } + + public string Mbid { get; set; } + + public string ArtistName { get; set; } + + public string ArtistMbid { get; set; } + + public LastImageSet ArtistImages { get; set; } + + public Uri ArtistUrl { get; set; } + + public Uri Url { get; set; } + + public LastImageSet Images { get; set; } + + public string AlbumName { get; set; } + + public int? ListenerCount { get; set; } + + public int? PlayCount { get; set; } + + public int? UserPlayCount { get; set; } + + public IEnumerable TopTags { get; set; } + + public DateTimeOffset? TimePlayed { get; set; } + + public bool? IsLoved { get; set; } + + public bool? IsNowPlaying { get; set; } + + public int? Rank { get; set; } + + #endregion + + /// + /// Parses the given JToken into a track + /// + /// A valid JToken + /// track equivalent to the JToken + /// If this method is used directly then the duration attribute will be parsed as MILLIseconds + internal static LastTrack ParseJToken(JToken token) + { + var t = new LastTrack(); + + t.Id = token.Value("id"); + t.Name = token.Value("name"); + t.Mbid = token.Value("mbid"); + + //some tracks do not contain the playcount prop, it will throw a FormatException + var playCountStr = token.Value("playcount"); + int playCount; + if (int.TryParse(playCountStr, out playCount)) + { + t.PlayCount = playCount; + } + + // same with userplaycount + int userPlayCount; + if(int.TryParse(token.Value("userplaycount"), out userPlayCount)) + { + t.UserPlayCount = userPlayCount; + } + + t.Url = new Uri(token.Value("url"), UriKind.Absolute); + + var artistToken = token.SelectToken("artist"); + if (artistToken.Type != JTokenType.String) + { + t.ArtistName = LastArtist.GetNameFromJToken(artistToken); + t.ArtistMbid = artistToken.Value("mbid"); + + if (artistToken.Value("url") != null) + { + t.ArtistUrl = new Uri(artistToken.Value("url"), UriKind.Absolute); + } + + var artistImages = artistToken.SelectToken("image", false); + if (artistImages != null) + { + t.ArtistImages = LastImageSet.ParseJToken(artistImages); + } + } + else + t.ArtistName = artistToken.ToObject(); + + var albumToken = token.SelectToken("album"); + if (albumToken != null) + { + t.AlbumName = LastAlbum.GetNameFromJToken(albumToken); + } + + var tagsToken = token.SelectToken("toptags"); + if (tagsToken != null) + { + var tagToken = tagsToken.SelectToken("tag"); + if (tagToken != null) + { + t.TopTags = + tagToken.Type == JTokenType.Array + ? tagToken.Children().Select(token1 => LastTag.ParseJToken(token1)) + : new List { LastTag.ParseJToken(tagToken) }; + } + } + + var date = token.SelectToken("date"); + if (date != null) + { + var stamp = date.Value("uts"); + t.TimePlayed = stamp.FromUnixTime(); + } + + var images = token.SelectToken("image"); + if (images != null) + { + var imageCollection = LastImageSet.ParseJToken(images); + t.Images = imageCollection; + } + + var lovedToken = token.SelectToken("userloved"); + var extendedLovedToken = token.SelectToken("loved"); + if (lovedToken != null) + { + t.IsLoved = Convert.ToBoolean(lovedToken.Value()); + } + else if (extendedLovedToken != null) + { + t.IsLoved = Convert.ToBoolean(extendedLovedToken.Value()); + } + + var attrToken = token.SelectToken("@attr"); + if (attrToken != null && attrToken.HasValues) + { + t.IsNowPlaying = attrToken.Value("nowplaying"); + t.Rank = attrToken.Value("rank"); + } + + // api returns milliseconds when track.getInfo is called directly + var secsStr = token.Value("duration"); + double secs; + + if (double.TryParse(secsStr, out secs)) + { + if (Math.Abs(secs - default(double)) > double.Epsilon) + { + t.Duration = TimeSpan.FromMilliseconds(secs); + } + } + + return t; + } + + /// + /// Parses the given JToken into a track + /// + /// A valid JToken + /// Name of the album this track belongs to + /// track equivalent to the JToken + /// If this method is used then the duration attribute will be parsed as seconds + internal static LastTrack ParseJToken(JToken token, string albumName) + { + var t = ParseJToken(token); + t.AlbumName = albumName; + + // the api returns seconds for this value when not track.getInfo + var secs = token.Value("duration"); + t.Duration = TimeSpan.FromSeconds(secs); + + return t; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastUser.cs b/IF.Lastfm.Core/Objects/LastUser.cs new file mode 100644 index 00000000..d0f081b7 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastUser.cs @@ -0,0 +1,74 @@ +using System; +using IF.Lastfm.Core.Api.Enums; +using Newtonsoft.Json.Linq; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Objects +{ + public class LastUser : ILastfmObject + { + public string Name { get; private set; } + public string FullName { get; private set; } + public LastImageSet Avatar { get; private set; } + public string Id { get; private set; } + public int Age { get; private set; } + public string Country { get; private set; } + public Gender Gender { get; private set; } + public bool IsSubscriber { get; private set; } + public int Playcount { get; private set; } + public int Playlists { get; private set; } + public DateTime TimeRegistered { get; private set; } + public int Bootstrap { get; private set; } + public string Type { get; private set; } + + /// + /// Parses the given to a + /// . + /// + /// JToken to parse. + /// Parsed LastUser. + internal static LastUser ParseJToken(JToken token) + { + var u = new LastUser + { + Name = token.Value("name"), + FullName = token.Value("realname"), + Country = token.Value("country"), + Id = token.Value("id"), + Playcount = token.Value("playcount"), + Playlists = token.Value("playlists"), + Gender = ParseGender(token.Value("gender")), + IsSubscriber = Convert.ToBoolean(token.Value("subscriber")), + TimeRegistered = token.Value("registered.unixtime").FromUnixTime().DateTime, + Bootstrap = token.Value("bootstrap"), + Type = token.Value("type") + }; + + var images = token.SelectToken("image"); + if (images != null) + { + u.Avatar = LastImageSet.ParseJToken(images); + } + + return u; + } + + /// + /// Parses the given string into a . + /// + /// String to parse. + /// Parsed . + internal static Gender ParseGender(string gender) + { + switch (gender) + { + case "m": + return Gender.Male; + case "f": + return Gender.Female; + default: + return Gender.Other; + } + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastUserSession.cs b/IF.Lastfm.Core/Objects/LastUserSession.cs new file mode 100644 index 00000000..b09e2959 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastUserSession.cs @@ -0,0 +1,17 @@ +using IF.Lastfm.Core.Json; +using Newtonsoft.Json; + +namespace IF.Lastfm.Core.Objects +{ + public class LastUserSession + { + [JsonProperty("name")] + public string Username { get; set; } + + [JsonProperty("key")] + public string Token { get; set; } + + [JsonProperty("subscriber"), JsonConverter(typeof(LastFmBooleanConverter))] + public bool IsSubscriber { get; set; } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastWeeklyChartList.cs b/IF.Lastfm.Core/Objects/LastWeeklyChartList.cs new file mode 100644 index 00000000..d4e0b726 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastWeeklyChartList.cs @@ -0,0 +1,38 @@ +using System; +using IF.Lastfm.Core.Api.Enums; +using Newtonsoft.Json.Linq; +using IF.Lastfm.Core.Api.Helpers; + +namespace IF.Lastfm.Core.Objects +{ + public class LastWeeklyChartList : ILastfmObject + { + public string Text { get; private set; } + public DateTime FromDate { get; private set; } + public double From { get; private set; } + public DateTime ToDate { get; private set; } + public double To { get; private set; } + + + /// + /// Parses the given to a + /// . + /// + /// JToken to parse. + /// Parsed LastWeeklyChartList. + internal static LastWeeklyChartList ParseJToken(JToken token) + { + var c = new LastWeeklyChartList + { + Text = token.Value("#name"), + FromDate = token.Value("from").FromUnixTime().DateTime, + From = token.Value("from"), + ToDate = token.Value("to").FromUnixTime().DateTime, + To = token.Value("to") + }; + + return c; + } + + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Objects/LastWiki.cs b/IF.Lastfm.Core/Objects/LastWiki.cs new file mode 100644 index 00000000..b133bb08 --- /dev/null +++ b/IF.Lastfm.Core/Objects/LastWiki.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json.Linq; +using System; + +namespace IF.Lastfm.Core.Objects +{ + /// + /// TODO YearFormed -> FormationList + /// "formationlist": { + /// "formation": { + /// "yearfrom": "2003", + /// "yearto": "" + /// } + /// } + /// + /// TODO links + /// "links": { + /// "link": { + /// "#text": "", + /// "rel": "original", + /// "href": "http://www.last.fm/music/Frightened+Rabbit/+wiki" + /// } + /// } + /// + public class LastWiki : ILastfmObject + { + #region Properties + + public DateTimeOffset Published { get; set; } + public string Summary { get; set; } + public string Content { get; set; } + public int YearFormed { get; set; } + + #endregion + + internal static LastWiki ParseJToken(JToken token) + { + var wiki = new LastWiki + { + Summary = token.Value("summary").Trim(), + Content = token.Value("content").Trim(), + YearFormed = token.Value("yearformed") + }; + + //Artist that do not contain an official bio will come with an empty published property. + //To avoid a parse exception, check if is null or empty. + if (!string.IsNullOrEmpty(token.Value("published"))) + wiki.Published = token.Value("published"); + + return wiki; + } + } +} diff --git a/IF.Lastfm.Core/Objects/Scrobble.cs b/IF.Lastfm.Core/Objects/Scrobble.cs new file mode 100644 index 00000000..fdbb4ec7 --- /dev/null +++ b/IF.Lastfm.Core/Objects/Scrobble.cs @@ -0,0 +1,93 @@ +using System; +using IF.Lastfm.Core.Api.Helpers; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace IF.Lastfm.Core.Objects +{ + public class Scrobble : IEquatable + { + /// + /// Not part of the Last.fm API. This is a convenience property allowing Scrobbles to have a unique ID. + /// IF.Lastfm.SQLite uses this field to store a primary key, if this Scrobble was cached. + /// Not used in Equals or GetHashCode implementations. + /// + public int Id { get; set; } + + public string IgnoredReason { get; private set; } + + public string Artist { get; private set; } + + public string AlbumArtist { get; set; } + + public string Album { get; private set; } + + public string Track { get; private set; } + + public DateTimeOffset TimePlayed { get; private set; } + + public bool ChosenByUser { get; set; } + + public TimeSpan? Duration { get; set; } + + public Scrobble() + { + } + + public Scrobble(string artist, string album, string track, DateTimeOffset timeplayed) + { + Artist = artist; + Album = album; + Track = track; + TimePlayed = timeplayed; + } + + internal static Scrobble ParseJToken(JToken token) + { + var album = token.SelectToken("album.#text")?.Value(); + var artist = token.SelectToken("artist.#text")?.Value(); + var track = token.SelectToken("track.#text")?.Value(); + var albumArtist = token.SelectToken("albumArtist.#text")?.Value(); + var timestamp = token.SelectToken("timestamp").Value(); + var ignoredMessage = token.SelectToken("ignoredMessage.#text")?.Value(); + + return new Scrobble(artist, album, track, timestamp.FromUnixTime()) + { + AlbumArtist = albumArtist, + IgnoredReason = ignoredMessage + }; + } + + public override bool Equals(object obj) + { + return Equals(obj as Scrobble); + } + + public bool Equals(Scrobble other) + { + return other != null && + IgnoredReason == other.IgnoredReason && + Artist == other.Artist && + AlbumArtist == other.AlbumArtist && + Album == other.Album && + Track == other.Track && + TimePlayed.Equals(other.TimePlayed) && + ChosenByUser == other.ChosenByUser && + EqualityComparer.Default.Equals(Duration, other.Duration); + } + + public override int GetHashCode() + { + var hashCode = 417801827; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(IgnoredReason); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Artist); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AlbumArtist); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Album); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Track); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(TimePlayed); + hashCode = hashCode * -1521134295 + ChosenByUser.GetHashCode(); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Duration); + return hashCode; + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Scrobblers/IScrobbler.cs b/IF.Lastfm.Core/Scrobblers/IScrobbler.cs new file mode 100644 index 00000000..f842128d --- /dev/null +++ b/IF.Lastfm.Core/Scrobblers/IScrobbler.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Scrobblers +{ + public interface IScrobbler + { + Task> GetCachedAsync(); + + Task ScrobbleAsync(Scrobble scrobble); + + Task ScrobbleAsync(IEnumerable scrobbles); + + Task SendCachedScrobblesAsync(); + } +} diff --git a/IF.Lastfm.Core/Scrobblers/MemoryScrobbler.cs b/IF.Lastfm.Core/Scrobblers/MemoryScrobbler.cs new file mode 100644 index 00000000..aec1da26 --- /dev/null +++ b/IF.Lastfm.Core/Scrobblers/MemoryScrobbler.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Scrobblers +{ + public class MemoryScrobbler : ScrobblerBase + { + private readonly HashSet _scrobbles; + + public MemoryScrobbler(ILastAuth auth, HttpClient httpClient = null) : base(auth, httpClient) + { + _scrobbles = new HashSet(); + } + + public override Task> GetCachedAsync() + { + return Task.FromResult(_scrobbles.AsEnumerable()); + } + + public override Task RemoveFromCacheAsync(ICollection scrobbles) + { + foreach (var scrobble in scrobbles) + { + _scrobbles.Remove(scrobble); + } + + return Task.FromResult(0); + } + + public override Task GetCachedCountAsync() + { + return Task.FromResult(_scrobbles.Count); + } + + protected override Task CacheAsync(IEnumerable scrobbles, LastResponseStatus reason) + { + foreach (var scrobble in scrobbles) + { + _scrobbles.Add(scrobble); + } + return Task.FromResult(LastResponseStatus.Cached); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Scrobblers/ScrobbleResponse.cs b/IF.Lastfm.Core/Scrobblers/ScrobbleResponse.cs new file mode 100644 index 00000000..b5f086dc --- /dev/null +++ b/IF.Lastfm.Core/Scrobblers/ScrobbleResponse.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Api.Helpers; +using IF.Lastfm.Core.Objects; +using Newtonsoft.Json.Linq; + +namespace IF.Lastfm.Core.Scrobblers +{ + public class ScrobbleResponse : LastResponse + { + public int AcceptedCount { get; internal set; } + + public IEnumerable Ignored { get; internal set; } + + public override bool Success + { + get + { + switch (Status) + { + case LastResponseStatus.Successful: + case LastResponseStatus.Cached: + return true; + default: + return false; + } + } + } + + public Exception Exception { get; internal set; } + + public ScrobbleResponse(LastResponseStatus status) + { + Status = status; + } + + public ScrobbleResponse() + { + Ignored = Enumerable.Empty(); + } + + public static Task CreateSuccessResponse(string json) + { + var root = JObject.Parse(json); + var scrobblesToken = root["scrobbles"]["scrobble"]; + var allItems = PageResponse.ParseItemsToken(scrobblesToken, Scrobble.ParseJToken).ToList(); + var ignored = allItems.Where(s => !String.IsNullOrEmpty(s.IgnoredReason)).ToList(); + + var acceptedCount = allItems.Count - ignored.Count; + + var response = new ScrobbleResponse(LastResponseStatus.Successful) + { + AcceptedCount = acceptedCount, + Ignored = ignored + }; + + return Task.FromResult(response); + } + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/Scrobblers/ScrobblerBase.cs b/IF.Lastfm.Core/Scrobblers/ScrobblerBase.cs new file mode 100644 index 00000000..ed9c3b9c --- /dev/null +++ b/IF.Lastfm.Core/Scrobblers/ScrobblerBase.cs @@ -0,0 +1,114 @@ +using IF.Lastfm.Core.Api; +using IF.Lastfm.Core.Api.Commands.Track; +using IF.Lastfm.Core.Api.Enums; +using IF.Lastfm.Core.Helpers; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using IF.Lastfm.Core.Objects; + +namespace IF.Lastfm.Core.Scrobblers +{ + public abstract class ScrobblerBase : ApiBase, IScrobbler + { + public event EventHandler AfterSend; + + internal int MaxBatchSize { get; set; } + + protected ScrobblerBase(ILastAuth auth, HttpClient httpClient = null) : base(httpClient) + { + Auth = auth; + + MaxBatchSize = 50; + } + + public Task ScrobbleAsync(Scrobble scrobble) + { + return ScrobbleAsync(new[] {scrobble}); + } + + public Task ScrobbleAsync(IEnumerable scrobbles) + { + return ScrobbleAsyncInternal(scrobbles); + } + + public Task SendCachedScrobblesAsync() + { + return ScrobbleAsyncInternal(Enumerable.Empty()); + } + + public async Task ScrobbleAsyncInternal(IEnumerable scrobbles) + { + var scrobblesList = new ReadOnlyCollection(scrobbles.ToList()); + var cached = await GetCachedAsync(); + var pending = scrobblesList.Concat(cached).OrderBy(s => s.TimePlayed).ToList(); + if (!pending.Any()) + { + return new ScrobbleResponse(LastResponseStatus.Successful); + } + + var batches = pending.Batch(MaxBatchSize); + var responses = new List(pending.Count % MaxBatchSize); + var responseExceptions = new List(); + foreach(var batch in batches) + { + var command = new ScrobbleCommand(Auth, batch) + { + HttpClient = HttpClient + }; + + try + { + var response = await command.ExecuteAsync(); + + var acceptedMap = new HashSet(pending); + foreach (var ignored in response.Ignored) + { + acceptedMap.Remove(ignored); + } + + await RemoveFromCacheAsync(acceptedMap); + + responses.Add(response); + } + catch (HttpRequestException httpEx) + { + responseExceptions.Add(httpEx); + } + } + + ScrobbleResponse scrobblerResponse; + if (!responses.Any() || responses.All(r => r.Success)) + { + scrobblerResponse = new ScrobbleResponse(LastResponseStatus.Successful); + } + else + { + var firstBadResponse = responses.FirstOrDefault(r => !r.Success && r.Status != LastResponseStatus.Unknown); + var originalResponseStatus = firstBadResponse?.Status ?? LastResponseStatus.RequestFailed; // TODO check httpEx + + var cacheStatus = await CacheAsync(scrobblesList, originalResponseStatus); + + scrobblerResponse = new ScrobbleResponse(cacheStatus); + } + + scrobblerResponse.Ignored = responses.SelectMany(r => r.Ignored); + scrobblerResponse.AcceptedCount = responses.Sum(r => r.AcceptedCount); + + AfterSend?.Invoke(this, scrobblerResponse); + + return scrobblerResponse; + } + + public abstract Task> GetCachedAsync(); + + public abstract Task RemoveFromCacheAsync(ICollection scrobbles); + + public abstract Task GetCachedCountAsync(); + + protected abstract Task CacheAsync(IEnumerable scrobble, LastResponseStatus reason); + } +} \ No newline at end of file diff --git a/IF.Lastfm.Core/packages.lock.json b/IF.Lastfm.Core/packages.lock.json new file mode 100644 index 00000000..6d716ce2 --- /dev/null +++ b/IF.Lastfm.Core/packages.lock.json @@ -0,0 +1,420 @@ +{ + "version": 1, + "dependencies": { + ".NETStandard,Version=v1.1": { + "NETStandard.Library": { + "type": "Direct", + "requested": "[1.6.1, )", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[9.0.1, )", + "resolved": "9.0.1", + "contentHash": "U82mHQSKaIk+lpSVCbWYKNavmNH1i5xrExDEquU1i6I5pV6UMOqRnJRSlKO3cMPfcpp0RgDY+8jUXHdQ4IfXvw==", + "dependencies": { + "Microsoft.CSharp": "4.0.1", + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Dynamic.Runtime": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Linq": "4.1.0", + "System.Linq.Expressions": "4.1.0", + "System.ObjectModel": "4.0.12", + "System.Reflection": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Serialization.Primitives": "4.1.1", + "System.Text.Encoding": "4.0.11", + "System.Text.Encoding.Extensions": "4.0.11", + "System.Text.RegularExpressions": "4.1.0", + "System.Threading": "4.0.11", + "System.Threading.Tasks": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11", + "System.Xml.XDocument": "4.0.11" + } + }, + "System.Net.Http": { + "type": "Direct", + "requested": "[4.3.2, )", + "resolved": "4.3.2", + "contentHash": "y7hv0o0weI0j0mvEcBOdt1F3CAADiWlcw3e54m8TfYiRmBPDIsHElx8QUPDlY4x6yWXKPGN0Z2TuXCTPgkm5WQ==", + "dependencies": { + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "17h8b5mXa87XYKrrVqdgZ38JefSUqLChUQpXgSnpzsM0nDOhE40FTeNWOJ/YmySGV6tG6T8+hjz6vxbknHJr6A==", + "dependencies": { + "System.Dynamic.Runtime": "4.0.11", + "System.Linq.Expressions": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.0.11", + "contentHash": "db34f6LHYM0U0JpE+sOmjar27BnqTVkbLJhgfwMpTdgTigG/Hna3m2MYVwnFzGGKnEJk2UXFuoVTr8WUbU91/A==", + "dependencies": { + "System.Linq.Expressions": "4.1.0", + "System.ObjectModel": "4.0.12", + "System.Reflection": "4.1.0", + "System.Runtime": "4.1.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + } + } + } +} \ No newline at end of file diff --git a/MusicX.sln b/MusicX.sln index 0f9bad8b..cabf933c 100644 --- a/MusicX.sln +++ b/MusicX.sln @@ -28,7 +28,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMediaToolkit", "FFMediaTo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PsCompiler", "PsCompiler\PsCompiler.csproj", "{449B9F89-C7C2-4C7E-9F2F-28CBB731E7F5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IF.Lastfm.Core", "lastfm\src\IF.Lastfm.Core\IF.Lastfm.Core.csproj", "{57464921-52F3-4172-83E9-0B7D75F9DDDF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IF.Lastfm.Core", "IF.Lastfm.Core\IF.Lastfm.Core.csproj", "{57464921-52F3-4172-83E9-0B7D75F9DDDF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/MusicX/MusicX.csproj b/MusicX/MusicX.csproj index b1d061c2..44baa111 100644 --- a/MusicX/MusicX.csproj +++ b/MusicX/MusicX.csproj @@ -109,7 +109,7 @@ - + diff --git a/MusicX/Services/Player/PlayerService.cs b/MusicX/Services/Player/PlayerService.cs index b2450410..af43d9f6 100644 --- a/MusicX/Services/Player/PlayerService.cs +++ b/MusicX/Services/Player/PlayerService.cs @@ -111,10 +111,12 @@ await Task.WhenAll( public async Task PlayTrackFromQueueAsync(int index) { var previousTrack = CurrentTrack!; + var previousPosition = player.Position; + await PlayTrackAsync(Tracks[index]); await Task.WhenAll( - _statsListeners.Select(b => b.TrackChangedAsync(previousTrack, CurrentTrack!, ChangeReason.TrackChange))); + _statsListeners.Select(b => b.TrackChangedAsync(previousTrack, CurrentTrack!, ChangeReason.TrackChange, previousPosition))); } private async Task PlayTrackAsync(PlaylistTrack track, TimeSpan? position = null) @@ -242,7 +244,7 @@ await Application.Current.Dispatcher.InvokeAsync(() => firstTrackTask = PlayTrackAsync(firstTrack, startPosition); await Task.WhenAll( - _statsListeners.Select(b => b.TrackChangedAsync(CurrentTrack, firstTrack, ChangeReason.PlaylistChange))); + _statsListeners.Select(b => b.TrackChangedAsync(CurrentTrack, firstTrack, ChangeReason.PlaylistChange, player.Position))); } Application.Current.Dispatcher.BeginInvoke( @@ -262,10 +264,12 @@ await Task.WhenAll( if (firstTrack is null) { var previousTrack = CurrentTrack; + var previousPosition = player.Position; + await PlayTrackAsync(Tracks[0], startPosition); await Task.WhenAll( - _statsListeners.Select(b => b.TrackChangedAsync(previousTrack, CurrentTrack!, ChangeReason.PlaylistChange))); + _statsListeners.Select(b => b.TrackChangedAsync(previousTrack, CurrentTrack!, ChangeReason.PlaylistChange, previousPosition))); } } catch (Exception e) @@ -375,11 +379,12 @@ async Task LoadMore() } var previousTrack = CurrentTrack!; + var previousPosition = player.Position; await PlayTrackAsync(nextTrack); await Task.WhenAll( - _statsListeners.Select(b => b.TrackChangedAsync(previousTrack, nextTrack, ChangeReason.NextButton))); + _statsListeners.Select(b => b.TrackChangedAsync(previousTrack, nextTrack, ChangeReason.NextButton, previousPosition))); }catch(Exception ex) { var properties = new Dictionary @@ -485,11 +490,12 @@ public async Task PreviousTrack() var previousTrack = Tracks[index]; var prevCurrentTrack = CurrentTrack!; + var previousPosition = player.Position; await PlayTrackAsync(previousTrack); await Task.WhenAll( - _statsListeners.Select(b => b.TrackChangedAsync(prevCurrentTrack, previousTrack!, ChangeReason.PrevButton))); + _statsListeners.Select(b => b.TrackChangedAsync(prevCurrentTrack, previousTrack!, ChangeReason.PrevButton, previousPosition))); } } catch (Exception e) diff --git a/MusicX/packages.lock.json b/MusicX/packages.lock.json index 0037fdc6..a03014f4 100644 --- a/MusicX/packages.lock.json +++ b/MusicX/packages.lock.json @@ -391,6 +391,16 @@ "resolved": "1.1.0", "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "4.7.0", @@ -428,6 +438,57 @@ "Microsoft.Windows.SDK.Win32Metadata": "55.0.45-preview" } }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.2", @@ -493,6 +554,15 @@ "Microsoft.NETCore.Targets": "1.1.0" } }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, "runtime.native.System.Net.Http": { "type": "Transitive", "resolved": "4.3.0", @@ -597,6 +667,26 @@ "SQLitePCLRaw.core": "2.1.5" } }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", @@ -638,6 +728,18 @@ "System.Security.Permissions": "6.0.0" } }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -660,6 +762,16 @@ "System.Threading": "4.3.0" } }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.Diagnostics.Tracing": { "type": "Transitive", "resolved": "4.3.0", @@ -724,6 +836,44 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, "System.IO.FileSystem": { "type": "Transitive", "resolved": "4.3.0", @@ -764,6 +914,30 @@ "System.Runtime.Extensions": "4.3.0" } }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Management": { "type": "Transitive", "resolved": "6.0.0", @@ -821,6 +995,31 @@ "System.Runtime.Handles": "4.3.0" } }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Reflection": { "type": "Transitive", "resolved": "4.3.0", @@ -838,6 +1037,38 @@ "resolved": "4.7.0", "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Reflection.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -848,6 +1079,15 @@ "System.Runtime": "4.3.0" } }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Resources.ResourceManager": { "type": "Transitive", "resolved": "4.3.0", @@ -907,6 +1147,20 @@ "System.Runtime.Handles": "4.3.0" } }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, "System.Runtime.Numerics": { "type": "Transitive", "resolved": "4.3.0", @@ -1096,11 +1350,30 @@ "System.Runtime": "4.3.0" } }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, "System.Threading": { "type": "Transitive", "resolved": "4.3.0", @@ -1125,6 +1398,26 @@ "System.Runtime": "4.3.0" } }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "npvJkVKl5rKXrtl1Kkm6OhOUaYGEiF9wFbppFRWSMoApKzt2PiPHT2Bb8a5sAWxprvdOAtvaARS9QYMznEUtug==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.ValueTuple": { "type": "Transitive", "resolved": "4.5.0", @@ -1138,6 +1431,47 @@ "System.Drawing.Common": "6.0.0" } }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, "VkNet": { "type": "Transitive", "resolved": "1.70.0", @@ -1166,6 +1500,7 @@ "if.lastfm.core": { "type": "Project", "dependencies": { + "NETStandard.Library": "[1.6.1, )", "Newtonsoft.Json": "[9.0.1, )", "System.Net.Http": "[4.3.2, )" } @@ -1278,6 +1613,11 @@ "System.Runtime": "4.3.0" } }, + "runtime.any.System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "S/GPBmfPBB48ZghLxdDR7kDAJVAqgAuThyDJho3OLP5OS4tWD2ydyL8LKm8lhiBxce10OKe9X2zZ6DUjAqEbPg==" + }, "runtime.any.System.Diagnostics.Tracing": { "type": "Transitive", "resolved": "4.3.0", @@ -1303,6 +1643,11 @@ "resolved": "4.3.0", "contentHash": "hLC3A3rI8jipR5d9k7+f0MgRCW6texsAp0MWkN/ci18FMtQ9KH7E2vDn/DH2LkxsszlpJpOn9qy6Z6/69rH6eQ==" }, + "runtime.any.System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cPhT+Vqu52+cQQrDai/V91gubXUnDKNRvlBnH+hOgtGyHdC17aQIU64EaehwAQymd7kJA5rSrVRNfDYrbhnzyA==" + }, "runtime.any.System.Reflection.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -1346,6 +1691,11 @@ "resolved": "4.3.0", "contentHash": "OhBAVBQG5kFj1S+hCEQ3TUHBAEtZ3fbEMgZMRNdN8A0Pj4x+5nTELEqL59DU0TjKVE6II3dqKw4Dklb3szT65w==" }, + "runtime.any.System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "w4ehZJ+AwXYmGwYu+rMvym6RvMaRiUEQR1u6dwcyuKHxz8Heu/mO9AG1MquEgTyucnhv3M43X0iKpDOoN17C0w==" + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -1361,6 +1711,16 @@ "resolved": "4.3.0", "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "runtime.win7-x64.runtime.native.System.IO.Compression": "4.3.0" + } + }, "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -1410,6 +1770,22 @@ "System.Runtime.InteropServices": "4.3.0" } }, + "runtime.win.System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RRACWygml5dnmfgC1SW6tLGsFgwsUAKFtvhdyHnIEz4EhWyrd7pacDdY95CacQJy7BMXRDRCejC9aCRC0Y1sQA==", + "dependencies": { + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, "runtime.win.System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1454,6 +1830,31 @@ "System.Threading": "4.3.0" } }, + "runtime.win.System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FK/2gX6MmuLIKNCGsV59Fe4IYrLrI5n9pQ1jh477wiivEM/NCXDT2dRetH5FSfY0bQ+VgTLcS3zcmjQ8my3nxQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Net.NameResolution": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Principal.Windows": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Overlapped": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, "runtime.win.System.Runtime.Extensions": { "type": "Transitive", "resolved": "4.3.0", @@ -1462,6 +1863,11 @@ "System.Private.Uri": "4.3.0" } }, + "runtime.win7-x64.runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UamDlgSO/nIzc96M+g3wbvAGbAuXjvRYR5Ttm/FVJgt2iva8ouOqSJ0j6eGI7pZDLvD/ZISl9XRZOajE/Xvizg==" + }, "runtime.win7.System.Private.Uri": { "type": "Transitive", "resolved": "4.3.0", @@ -1495,6 +1901,19 @@ "runtime.any.System.Collections": "4.3.0" } }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.win.System.Console": "4.3.0" + } + }, "System.Diagnostics.Debug": { "type": "Transitive", "resolved": "4.3.0", @@ -1506,6 +1925,17 @@ "runtime.win.System.Diagnostics.Debug": "4.3.0" } }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Diagnostics.Tools": "4.3.0" + } + }, "System.Diagnostics.Tracing": { "type": "Transitive", "resolved": "4.3.0", @@ -1566,6 +1996,28 @@ "runtime.any.System.IO": "4.3.0" } }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, "System.IO.FileSystem": { "type": "Transitive", "resolved": "4.3.0", @@ -1623,6 +2075,27 @@ "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" } }, + "System.Net.NameResolution": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "AFYl08R7MrsrEjqpQWTZWBadqXyTzNDaWpMqyxhb0d6sGhV6xMDKueuBXlLL30gz+DIRY6MpdgnHWlCh5wmq9w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Principal.Windows": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, "System.Net.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -1635,6 +2108,20 @@ "runtime.win.System.Net.Primitives": "4.3.0" } }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.win.System.Net.Sockets": "4.3.0" + } + }, "System.Private.Uri": { "type": "Transitive", "resolved": "4.3.0", @@ -1658,6 +2145,18 @@ "runtime.any.System.Reflection": "4.3.0" } }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Reflection.Extensions": "4.3.0" + } + }, "System.Reflection.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -1728,6 +2227,20 @@ "runtime.any.System.Runtime.InteropServices": "4.3.0" } }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "6.0.0", @@ -1901,15 +2414,6 @@ "resolved": "8.0.0", "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", - "dependencies": { - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, "System.Threading.Overlapped": { "type": "Transitive", "resolved": "4.3.0", @@ -1932,6 +2436,17 @@ "runtime.any.System.Threading.Tasks": "4.3.0" } }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "runtime.any.System.Threading.Timer": "4.3.0" + } + }, "System.Windows.Extensions": { "type": "Transitive", "resolved": "6.0.0", diff --git a/lastfm b/lastfm deleted file mode 160000 index 677deaf6..00000000 --- a/lastfm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 677deaf6f4a1fc77aeef691eda068a05d039eb0b From 2bf2dab10efd1e15dd79b16fd53ab1527ccae239 Mon Sep 17 00:00:00 2001 From: zznty <94796179+zznty@users.noreply.github.com> Date: Sun, 28 Apr 2024 19:28:49 +0700 Subject: [PATCH 18/18] fix: catch json exceptions for config --- MusicX/Services/ConfigService.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/MusicX/Services/ConfigService.cs b/MusicX/Services/ConfigService.cs index cab82f12..24e0666f 100644 --- a/MusicX/Services/ConfigService.cs +++ b/MusicX/Services/ConfigService.cs @@ -78,6 +78,10 @@ public async Task GetConfig() await using var stream = File.OpenRead(_configPath); config = await JsonSerializer.DeserializeAsync(stream, _configSerializerOptions); } + catch (JsonException e) + { + _logger.Error(e, "Failed to read config"); + } finally { ConfigSemaphore.Release(); @@ -102,6 +106,10 @@ public async Task SetConfig(ConfigModel config) await using var stream = File.Create(_configPath); await JsonSerializer.SerializeAsync(stream, config, _configSerializerOptions); } + catch (JsonException e) + { + _logger.Error(e, "Failed to write config"); + } finally { ConfigSemaphore.Release();