From 6a52d1124b0acbe5e1ef3fec44c9f075ae609bee Mon Sep 17 00:00:00 2001 From: ashpynov Date: Tue, 14 May 2024 16:36:56 +0300 Subject: [PATCH 01/20] Added very basic support of theme Pause/Resume music and Music name --- Controls/MusicControl.xaml | 14 + Controls/MusicControl.xaml.cs | 69 +++ PlayniteSounds.cs | 47 +- PlayniteSounds.csproj | 819 +++++++++++++++++----------------- README.md | 68 +++ 5 files changed, 610 insertions(+), 407 deletions(-) create mode 100644 Controls/MusicControl.xaml create mode 100644 Controls/MusicControl.xaml.cs diff --git a/Controls/MusicControl.xaml b/Controls/MusicControl.xaml new file mode 100644 index 0000000..03e7c1f --- /dev/null +++ b/Controls/MusicControl.xaml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/Controls/MusicControl.xaml.cs b/Controls/MusicControl.xaml.cs new file mode 100644 index 0000000..998a3e7 --- /dev/null +++ b/Controls/MusicControl.xaml.cs @@ -0,0 +1,69 @@ +using Playnite.SDK; +using Playnite.SDK.Controls; +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows; + +namespace PlayniteSounds.Controls +{ + public partial class MusicControl : PluginUserControl, INotifyPropertyChanged + { + IPlayniteAPI PlayniteApi; + private PlayniteSounds _playniteSounds; + + static MusicControl() + { + TagProperty.OverrideMetadata(typeof(MusicControl), new FrameworkPropertyMetadata(-1, OnTagChanged)); + } + + public MusicControl(IPlayniteAPI PlayniteApi, PlayniteSounds plugin) + { + this.PlayniteApi = PlayniteApi; + InitializeComponent(); + DataContext = this; + _playniteSounds = plugin; + } + private static void OnTagChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + (d as MusicControl).VideoIsPlaying = Convert.ToBoolean(e.NewValue); + } + + public event PropertyChangedEventHandler PropertyChanged; + public void OnPropertyChanged([CallerMemberName] string name = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + } + + public RelayCommand VideoPlayingCommand + => new RelayCommand(videoIsPlaying => VideoIsPlaying = videoIsPlaying); + + private string _currentMusicName=string.Empty; + public string CurrentMusicName + { + get => _currentMusicName; + set + { + _currentMusicName = value; + OnPropertyChanged(nameof(CurrentMusicName)); + } + } + + private bool _videoIsPlaying = false; + public bool VideoIsPlaying + { + get => _videoIsPlaying; + set + { + _videoIsPlaying = value; + if (_videoIsPlaying) + _playniteSounds.MusicPause(); + else + _playniteSounds.MusicResume(); + + OnPropertyChanged(nameof(VideoPlayingCommand)); + OnPropertyChanged(nameof(VideoIsPlaying)); + } + } + } +} diff --git a/PlayniteSounds.cs b/PlayniteSounds.cs index 55f3a4c..fb89e31 100644 --- a/PlayniteSounds.cs +++ b/PlayniteSounds.cs @@ -19,6 +19,7 @@ using PlayniteSounds.Common; using PlayniteSounds.Common.Constants; using PlayniteSounds.Models; +using PlayniteSounds.Controls; namespace PlayniteSounds { @@ -79,6 +80,11 @@ public class PlayniteSounds : GenericPlugin private ISet _pausers = new HashSet(); + private List _MusicControls = new List(); + + public void MusicResume() => ResumeMusic(); + public void MusicPause() => PauseMusic(); + #region Constructor public PlayniteSounds(IPlayniteAPI api) : base(api) @@ -145,6 +151,17 @@ public PlayniteSounds(IPlayniteAPI api) : base(api) PlayniteApi.Database.Platforms.ItemCollectionChanged += UpdatePlatforms; PlayniteApi.Database.FilterPresets.ItemCollectionChanged += UpdateFilters; PlayniteApi.UriHandler.RegisterSource("Sounds", HandleUriEvent); + + #region Control constructor + + AddCustomElementSupport(new AddCustomElementSupportArgs + { + SourceName = "Sounds", + ElementList = new List { "MusicControl" } + }); + + #endregion + } catch (Exception e) { @@ -159,6 +176,27 @@ public void UpdateDownloadManager(PlayniteSoundsSettings settings) private static string HelpLine(string baseMessage) => $"{SoundFile.DesktopPrefix}{baseMessage} - {SoundFile.FullScreenPrefix}{baseMessage}\n"; + #region Control registration + + public override Control GetGameViewControl(GetGameViewControlArgs args) + { + var strArgs = args.Name.Split('_'); + + var controlType = strArgs[0]; + + switch (controlType) + { + case "MusicControl": + _MusicControls.Add(new MusicControl(PlayniteApi, this)); + return _MusicControls.Last(); + default: + throw new ArgumentException($"Unrecognized controlType '{controlType}' for request '{args.Name}'"); + } + } + + #endregion + + #endregion #region Playnite Interface @@ -572,6 +610,7 @@ private void SubCloseMusic() _musicPlayer.Clock.Controller.Stop(); _musicPlayer.Clock = null; _musicPlayer.Close(); + _MusicControls.ForEach( c => c.CurrentMusicName = string.Empty); } private void ForcePlayMusicFromPath(string filePath) @@ -607,6 +646,7 @@ private void SubPlayMusicFromPath(string filePath) _musicPlayer.Clock = _timeLine.CreateClock(); _musicPlayer.Clock.Controller.Begin(); _musicEnded = false; + _MusicControls.ForEach(c => c.CurrentMusicName = Path.GetFileNameWithoutExtension(filePath)); } } @@ -1760,7 +1800,11 @@ private bool ShouldPlayMusicOrClose() private bool ShouldPlaySound() => ShouldPlayAudio(Settings.SoundState); - private bool ShouldPlayMusic() => _pausers.Count is 0 && !_gameRunning && ShouldPlayAudio(Settings.MusicState); + private bool ShouldPlayMusic() => + _pausers.Count is 0 + && _MusicControls.All(c => c.VideoIsPlaying==false) + && !_gameRunning + && ShouldPlayAudio(Settings.MusicState); private bool ShouldPlayAudio(AudioState state) { @@ -1865,5 +1909,6 @@ public void HandleException(Exception e) private IDialogsFactory Dialogs => PlayniteApi.Dialogs; #endregion + } } diff --git a/PlayniteSounds.csproj b/PlayniteSounds.csproj index 1ed17f2..ef77620 100644 --- a/PlayniteSounds.csproj +++ b/PlayniteSounds.csproj @@ -1,407 +1,414 @@ - - - - - Debug - AnyCPU - {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F} - Library - Properties - PlayniteSounds - PlayniteSounds - v4.6.2 - 512 - true - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - packages\AngleSharp.0.17.1\lib\net461\AngleSharp.dll - - - packages\CliWrap.3.4.4\lib\net461\CliWrap.dll - - - packages\HtmlAgilityPack.1.11.46\lib\Net45\HtmlAgilityPack.dll - - - packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll - - - packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll - - - packages\PlayniteSDK.6.4.0\lib\net462\Playnite.SDK.dll - - - - - - packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - - - - packages\System.Linq.Async.6.0.1\lib\netstandard2.0\System.Linq.Async.dll - - - - packages\System.Memory.4.5.5\lib\net461\System.Memory.dll - - - - packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll - - - packages\System.Text.Encoding.CodePages.6.0.0\lib\net461\System.Text.Encoding.CodePages.dll - - - packages\System.Text.Encodings.Web.6.0.0\lib\net461\System.Text.Encodings.Web.dll - - - packages\System.Text.Json.6.0.5\lib\net461\System.Text.Json.dll - - - packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - - - - - - - - - - - packages\YoutubeExplode.6.2.2\lib\net461\YoutubeExplode.dll - - - packages\YoutubeExplode.Converter.6.2.2\lib\net461\YoutubeExplode.Converter.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PlayniteSoundsSettingsView.xaml - - - - - - - PreserveNewest - - - - - - - MSBuild:Compile - Designer - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - MSBuild:Compile - Designer - Always - - - Designer - MSBuild:Compile - - - - - PreserveNewest - - - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - Always - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - + + + + + Debug + AnyCPU + {4FDF1E89-5BC3-4C72-8FDA-0D580E7A5D5F} + Library + Properties + PlayniteSounds + PlayniteSounds + v4.6.2 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + packages\AngleSharp.0.17.1\lib\net461\AngleSharp.dll + + + packages\CliWrap.3.4.4\lib\net461\CliWrap.dll + + + packages\HtmlAgilityPack.1.11.46\lib\Net45\HtmlAgilityPack.dll + + + packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + + + packages\PlayniteSDK.6.4.0\lib\net462\Playnite.SDK.dll + + + + + + packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + + + + packages\System.Linq.Async.6.0.1\lib\netstandard2.0\System.Linq.Async.dll + + + + packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + + packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll + + + packages\System.Text.Encoding.CodePages.6.0.0\lib\net461\System.Text.Encoding.CodePages.dll + + + packages\System.Text.Encodings.Web.6.0.0\lib\net461\System.Text.Encodings.Web.dll + + + packages\System.Text.Json.6.0.5\lib\net461\System.Text.Json.dll + + + packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + + + + + + + + + packages\YoutubeExplode.6.2.2\lib\net461\YoutubeExplode.dll + + + packages\YoutubeExplode.Converter.6.2.2\lib\net461\YoutubeExplode.Converter.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PlayniteSoundsSettingsView.xaml + + + + MusicControl.xaml + + + + + + PreserveNewest + + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + Always + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + + + PreserveNewest + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + \ No newline at end of file diff --git a/README.md b/README.md index 8a39800..ed3410c 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,71 @@ Thanks to the following people who have contributed with translations: * Original Localization file loader by [Lacro59](https://github.com/Lacro59) * Sound Manager by [dfirsht](https://github.com/dfirsht) * Downloader Manager by [cnapolit](https://github.com/cnapolit) + +## Theme Integration +Extension expose custom control to manage Music playing: ```Sounds_MusicControl```. Using this control it is possible to: +* **Pause/Resume music** for example during trailer playback +* **Retrieve current Music filename** to show music title + +### Show current Music name + +Current Music filename (no path, no extension) is exposed via Content.CurrentMusicName property. you may use in via binding: + +```xml + + +``` +) + +### Pause/Resume during trailer playback +Control track Tag property manipulation and pause Music on 'True' value and Resume on false. +
+ Here how it can be implemented in theme GameDetail.xaml + +```xml + +``` +
From edc8ab6ba924dab91ef69b45af5fd2120dee4f18 Mon Sep 17 00:00:00 2001 From: ashpynov Date: Thu, 16 May 2024 19:29:42 +0300 Subject: [PATCH 02/20] Reimplemented YoutubeDownloader without Youtube Explode --- .gitignore | 3 + Downloaders/DownloadManager.cs | 5 +- Downloaders/YoutubeClient.cs | 205 ++++++++++++++++++++ Downloaders/YtDownloader.cs | 253 ++++++++++++------------- Localization/LocSource.xaml | 8 +- Localization/en_US.xaml | 7 +- Models/PlayniteSoundSettings.cs | 100 ++++++---- PlayniteSounds.cs | 10 +- PlayniteSounds.csproj | 19 +- PlayniteSoundsSettingsView.xaml | 76 +++++--- PlayniteSoundsSettingsView.xaml.cs | 3 +- PlayniteSoundsSettingsViewModel.cs | 294 +++++++++++++++-------------- packages.config | 38 ++-- 13 files changed, 652 insertions(+), 369 deletions(-) create mode 100644 Downloaders/YoutubeClient.cs diff --git a/.gitignore b/.gitignore index 0d02b42..2be6572 100644 --- a/.gitignore +++ b/.gitignore @@ -361,3 +361,6 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd /playnite +.vscode/launch.json +.vscode/settings.json +.vscode/tasks.json diff --git a/Downloaders/DownloadManager.cs b/Downloaders/DownloadManager.cs index 186ad51..997196e 100644 --- a/Downloaders/DownloadManager.cs +++ b/Downloaders/DownloadManager.cs @@ -30,9 +30,10 @@ public DownloadManager(PlayniteSoundsSettings settings) public IEnumerable GetAlbumsForGame(string gameName, Source source, bool auto = false) { - if ((source is Source.All || source is Source.Youtube) && string.IsNullOrWhiteSpace(_settings.FFmpegPath)) + if ((source is Source.All || source is Source.Youtube) + && (string.IsNullOrWhiteSpace(_settings.FFmpegPath) || string.IsNullOrWhiteSpace(_settings.YtDlpPath))) { - throw new Exception("Cannot download from Youtube without the FFmpeg Path specified in settings."); + throw new Exception("Cannot download from Youtube without the FFmpeg and YT-DLP Paths specified in settings."); } if (source is Source.All) diff --git a/Downloaders/YoutubeClient.cs b/Downloaders/YoutubeClient.cs new file mode 100644 index 0000000..00ce90a --- /dev/null +++ b/Downloaders/YoutubeClient.cs @@ -0,0 +1,205 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace PlayniteSounds.Downloaders +{ + public class YoutubeItem + { + + public string Id { get; set; } + public string Title { get; set; } + public Uri ThumbnailUrl { get; set; } + public TimeSpan Duration { get; set; } + public int Index { get; set; } + public uint Count { get; set; } + } + + public class YoutubeClient + { + private readonly HttpClient _httpClient; + + List _items; + public List Results { get => _items; } + + private const string youtubeItemsListPathPlaylist = "..playlistRenderer"; + private const string youtubeContinuationTokenPath = "..continuationCommand.token"; + private const string youtubeVisitorDataPath = "..visitorData"; + private const string youtubeplaylistVideoPath = "..playlistPanelVideoRenderer"; + + private const string searchTypePlaylist = "EgIQAw%3D%3D"; + + public YoutubeClient(HttpClient httpClient) + { + _items = new List(); + _httpClient = httpClient; + } + + #region Search Album + + private string continuationToken = null; + + public List Search(string searchQuery, int maxNumber = 20) + { + continuationToken = null; + _items = new List(); + + do + { + var result = GetSearchResponseAsync(searchQuery, searchTypePlaylist, continuationToken).Result; + ParseSearchResult(result); + } while (continuationToken != null && continuationToken != "" && Results.Count() < maxNumber); + return Results; + } + + private async Task GetSearchResponseAsync( + string searchQuery, + string searchFilter, + string continuationToken = null, + CancellationToken cancellationToken = default + ) + { + var request = new HttpRequestMessage( + HttpMethod.Post, + "https://www.youtube.com/youtubei/v1/search" + ); + + var content = $@"{{ " + + (continuationToken == null + ? $@" ""query"": ""{WebUtility.UrlEncode(searchQuery)}"", + ""params"": ""{searchFilter}""," + : $@"""continuation"": ""{continuationToken}""," + ) + + $@" ""context"": {{ + ""client"": {{ + ""clientName"": ""WEB"", + ""clientVersion"": ""2.20210408.08.00"", + ""hl"": ""en"", + ""gl"": ""US"", + ""utcOffsetMinutes"": 0 + }} + }} + }}"; + + request.Content = new StringContent(content); + + var response = await _httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + private void ParseSearchResult(string json) + { + JObject jo = JObject.Parse(json); + var playlists = jo.SelectTokens(youtubeItemsListPathPlaylist).ToList(); + playlists.ForEach(x => _items.Add(new YoutubeItem + { + Id = x.SelectToken("playlistId")?.ToString(), + Title = x.SelectToken("title.simpleText")?.ToString(), + ThumbnailUrl = new Uri(x.SelectToken("thumbnails[0].thumbnails[0].url")?.ToString()), + Count = uint.Parse(x.SelectToken("videoCount").ToString()) + })); + + continuationToken = jo.SelectToken(youtubeContinuationTokenPath)?.ToString(); + } + + #endregion + + #region List Album songs + private HashSet encounteredIds = new HashSet(); + private string lastVideoId = null; + private int lastVideoIndex = 0; + private string visitorData = null; + + public List Playlist(string playlistId) + { + _items = new List(); + var encounteredIds = new HashSet(); + lastVideoId = null; + lastVideoIndex = 0; + visitorData = null; + string result; + do + { + result = GetPlaylistNextResponseAsync(playlistId, lastVideoId, lastVideoIndex, visitorData).Result; + + } while (ParsePlaylist(result) > 0); + + return Results; + } + + private async Task GetPlaylistNextResponseAsync( + string playlistId, + string videoId = null, + int index = 0, + string visitorData = null, + CancellationToken cancellationToken = default + ) + { + var request = new HttpRequestMessage( + HttpMethod.Post, + "https://www.youtube.com/youtubei/v1/next" + ); + request.Content = new StringContent( + $@" + {{ + ""playlistId"": ""{playlistId}"", + ""videoId"": ""{videoId}"", + ""playlistIndex"": {index}, + ""context"": {{ + ""client"": {{ + ""clientName"": ""WEB"", + ""clientVersion"": ""2.20210408.08.00"", + ""hl"": ""en"", + ""gl"": ""US"", + ""utcOffsetMinutes"": 0, + ""visitorData"": ""{visitorData}"" + }} + }} + }} + " + ); + + var response = await _httpClient.SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(); + } + + private int ParsePlaylist(string json) + { + JObject jo = JObject.Parse(json); + + visitorData = jo.SelectToken(youtubeVisitorDataPath)?.ToString(); + var videos = jo.SelectTokens(youtubeplaylistVideoPath).ToList(); + + var newItems = 0; + foreach (var x in videos) + { + var item = new YoutubeItem + { + Id = x.SelectToken("videoId")?.ToString(), + Title = x.SelectToken("title.simpleText")?.ToString(), + ThumbnailUrl = new Uri(x.SelectToken("thumbnail.thumbnails[0].url")?.ToString()), + Duration = TimeSpan.Parse(x.SelectToken("lengthText.simpleText")?.ToString()), + Index = (int)x.SelectToken("navigationEndpoint.watchEndpoint.index") + }; + if (!encounteredIds.Add(item.Id)) + continue; + newItems++; + _items.Add(item); + lastVideoId = item.Id; + lastVideoIndex = item.Index; + } + return newItems; + } + + #endregion + + } +} diff --git a/Downloaders/YtDownloader.cs b/Downloaders/YtDownloader.cs index f05ba6e..5160052 100644 --- a/Downloaders/YtDownloader.cs +++ b/Downloaders/YtDownloader.cs @@ -1,128 +1,125 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; -using Playnite.SDK; -using PlayniteSounds.Models; -using YoutubeExplode; -using YoutubeExplode.Common; -using YoutubeExplode.Converter; -using YoutubeExplode.Search; - -namespace PlayniteSounds.Downloaders -{ - internal class YtDownloader : IDownloader - { - private static readonly ILogger Logger = LogManager.GetLogger(); - - private readonly YoutubeClient _youtubeClient; - private readonly PlayniteSoundsSettings _settings; - - public YtDownloader(HttpClient httpClient, PlayniteSoundsSettings settings) - { - _youtubeClient = new YoutubeClient(httpClient); - _settings = settings; - } - - private const string BaseYtUrl = "https://www.youtube.com"; - public string BaseUrl() => BaseYtUrl; - - private const Source DlSource = Source.Youtube; - public Source DownloadSource() => DlSource; - - public IEnumerable GetAlbumsForGame(string gameName, bool auto = false) - => GetAlbumsFromExplodeApiAsync(gameName, auto).Result; - - public IEnumerable GetSongsFromAlbum(Album album) - => album.Songs ?? GetSongsFromExplodeApiAsync(album).ToEnumerable(); - - public bool DownloadSong(Song song, string path) => DownloadSongExplodeAsync(song, path).Result; - - private async Task> GetAlbumsFromExplodeApiAsync(string gameName, bool auto) - { - if (auto) - { - gameName += " Soundtrack"; - } - - var albums = new List(); - var videos = new List(); - - var videoResults = _youtubeClient.Search.GetResultBatchesAsync(gameName, SearchFilter.Video); - var videoEnumerator = videoResults.GetAsyncEnumerator(); - - for (var i = 0; i < 1 && await videoEnumerator.MoveNextAsync(); i++) - { - var batchOfVideos = - from VideoSearchResult videoSearchResult in videoEnumerator.Current.Items - select new Song - { - Name = videoSearchResult.Title, - Id = videoSearchResult.Id, - Length = videoSearchResult.Duration, - Source = DlSource, - IconUrl = videoSearchResult.Thumbnails.FirstOrDefault()?.Url - }; - - videos.AddRange(batchOfVideos); - - await videoEnumerator.MoveNextAsync(); - } - - if (videos.Any()) albums.Add(new Album - { - Name = Common.Constants.Resource.YoutubeSearch, - Songs = videos, - Source = DlSource - }); - - if (_settings.YtPlaylists) - { - var playlistResults = _youtubeClient.Search.GetResultBatchesAsync(gameName, SearchFilter.Playlist); - - var playlistEnumerator = playlistResults.GetAsyncEnumerator(); - for (var i = 0; i < 1 && await playlistEnumerator.MoveNextAsync(); i++) - { - var batchOfPlaylists = - from PlaylistSearchResult playlistSearchResult in playlistEnumerator.Current.Items - select new Album - { - Name = playlistSearchResult.Title, - Id = playlistSearchResult.Id, - Source = DlSource, - IconUrl = playlistSearchResult.Thumbnails.FirstOrDefault()?.Url - }; - - albums.AddRange(batchOfPlaylists); - } - } - - return albums; - } - - private IAsyncEnumerable GetSongsFromExplodeApiAsync(Album album) - => _youtubeClient.Playlists.GetVideosAsync(album.Id).Select(video => new Song - { - Name = video.Title, - Id = video.Id, - Length = video.Duration, - Source = DlSource, - IconUrl= video.Thumbnails.FirstOrDefault()?.Url - }); - - private async Task DownloadSongExplodeAsync(Song song, string path) - { - try - { - await _youtubeClient.Videos.DownloadAsync(song.Id, path, o => o.SetFFmpegPath(_settings.FFmpegPath)); - return true; - } - catch (Exception e) - { - Logger.Error(e, $"Something went wrong when attempting to download from Youtube with Id '{song.Id}' and Path '{path}'"); - return false; - } - } - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using Playnite.SDK; +using PlayniteSounds.Models; + + +namespace PlayniteSounds.Downloaders +{ + internal class YtDownloader : IDownloader + { + private static readonly ILogger Logger = LogManager.GetLogger(); + + private readonly PlayniteSoundsSettings _settings; + private readonly HttpClient _httpClient; + + public YtDownloader(HttpClient httpClient, PlayniteSoundsSettings settings) + { + _settings = settings; + _httpClient = httpClient; + } + + private const string BaseYtUrl = "https://www.youtube.com"; + public string BaseUrl() => BaseYtUrl; + + private const string youtubeDLArg = " -x --audio-format mp3 --audio-quality 0 --ffmpeg-location=\"{0}\" -o \"{3}\" {1}/watch?v={2}"; + + private const Source DlSource = Source.Youtube; + public Source DownloadSource() => DlSource; + + public IEnumerable GetAlbumsForGame(string gameName, bool auto = false) + => GetAlbumsFromYoutubeClient(gameName, auto); + + public IEnumerable GetSongsFromAlbum(Album album) + => album.Songs ?? GetSongsFromYoutubeClient(album); + + public bool DownloadSong(Song song, string path) => DownloadSongFromYoutubeDl(song, path); + + private IEnumerable GetAlbumsFromYoutubeClient(string gameName, bool auto) + { + try + { + return new YoutubeClient(_httpClient) + .Search(gameName + (auto ? " Soundtrack" : ""), 100) + .Select(x => + new Album + { + Name = x.Title, + Id = x.Id, + Source = Source.Youtube, + IconUrl = x.ThumbnailUrl.ToString(), + Count = x.Count + }) + .ToList(); + } + catch {} + + return null; + } + + private IEnumerable GetSongsFromYoutubeClient(Album album) + { + try + { + return new YoutubeClient(_httpClient) + .Playlist(album.Id) + .Select(x => new Song + { + Name = x.Title, + Id = x.Id, + Length = x.Duration, + Source = DlSource, + IconUrl = x.ThumbnailUrl.ToString() + }); + } + catch {} + return null; + } + + private bool DownloadSongFromYoutubeDl(Song song, string path) + { + string downloader = _settings.YtDlpPath; + string arguments = string.Format(youtubeDLArg, _settings.FFmpegPath, BaseUrl(), song.Id, path); + string workDir = Path.GetDirectoryName(_settings.YtDlpPath); + + try + { + Logger.Debug($"Starting downloader: {downloader}, {arguments}, {workDir}"); + var startupPath = downloader; + if (downloader.Contains("..")) + { + startupPath = Path.GetFullPath(downloader); + } + + var info = new ProcessStartInfo(startupPath) + { + Arguments = arguments, + WorkingDirectory = string.IsNullOrEmpty(workDir) ? (new FileInfo(startupPath)).Directory.FullName : workDir, + CreateNoWindow = true, + UseShellExecute = false + }; + + int result = 0; + using (var proc = Process.Start(info)) + { + proc.WaitForExit(); + result = proc.ExitCode; + if (result != 0) + { + throw new Exception($"Fail to download. Error code is {result}"); + } + } + } + catch (Exception e) + { + Logger.Error(e, $"Something went wrong when attempting to download from Youtube with Id '{song.Id}' and Path '{path}'"); + return false; + } + return true; + } + } +} diff --git a/Localization/LocSource.xaml b/Localization/LocSource.xaml index d171f51..c9115a1 100644 --- a/Localization/LocSource.xaml +++ b/Localization/LocSource.xaml @@ -26,9 +26,10 @@ Automatically normalize music when downloading Tag Normalized Games - FFmpeg Path + ffmpeg path: FFmpeg is required for audio normalization and downloading - FFmpeg-Normalize Path + + ffmpeg-normalize Path Normalizing audio allows consistent volume between tracks without compromising quality FFmpeg-Normalize Documentation (Optional) FFmpeg-Normalize Custom Arguments @@ -40,6 +41,9 @@ Manual: Display Results Side-By-Side Auto: Select Results Side-By-Side + yt-dlp path: + yt-dlp is required for downloading audio from Youtube + Never Desktop Fullscreen diff --git a/Localization/en_US.xaml b/Localization/en_US.xaml index d171f51..a155b5a 100644 --- a/Localization/en_US.xaml +++ b/Localization/en_US.xaml @@ -26,9 +26,9 @@ Automatically normalize music when downloading Tag Normalized Games - FFmpeg Path + ffmpeg path: FFmpeg is required for audio normalization and downloading - FFmpeg-Normalize Path + ffmpeg-normalize path: Normalizing audio allows consistent volume between tracks without compromising quality FFmpeg-Normalize Documentation (Optional) FFmpeg-Normalize Custom Arguments @@ -40,6 +40,9 @@ Manual: Display Results Side-By-Side Auto: Select Results Side-By-Side + yt-dlp path: + yt-dlp is required for downloading audio from Youtube + Never Desktop Fullscreen diff --git a/Models/PlayniteSoundSettings.cs b/Models/PlayniteSoundSettings.cs index 0761469..aaafb6a 100644 --- a/Models/PlayniteSoundSettings.cs +++ b/Models/PlayniteSoundSettings.cs @@ -1,32 +1,68 @@ -using System; -using System.Collections.Generic; - -namespace PlayniteSounds.Models -{ - public class PlayniteSoundsSettings - { - public AudioState MusicState { get; set; } = AudioState.Always; - public AudioState SoundState { get; set; } = AudioState.Always; - public MusicType MusicType { get; set; } = MusicType.Game; - public int MusicVolume { get; set; } = 25; - public bool StopMusic { get; set; } = true; - public bool SkipFirstSelectSound { get; set; } - public bool PlayBackupMusic { get; set; } - public bool PauseOnDeactivate { get; set; } = true; - public bool RandomizeOnEverySelect { get; set; } - public bool RandomizeOnMusicEnd { get; set; } = true; - public bool TagMissingEntries { get; set; } - public bool AutoDownload { get; set; } - public bool AutoParallelDownload { get; set; } - public bool ManualParallelDownload { get; set; } = true; - public bool YtPlaylists { get; set; } = true; - public bool NormalizeMusic { get; set; } = true; - public bool TagNormalizedGames { get; set; } - public string FFmpegPath { get; set; } - public string FFmpegNormalizePath { get; set; } - public string FFmpegNormalizeArgs { get; set; } - public IList Downloaders { get; set; } = new List { Source.Youtube }; - public DateTime LastAutoLibUpdateAssetsDownload { get; set; } = DateTime.Now; - public bool PromptedForMigration { get; set; } - } -} +using Playnite.SDK.Data; +using System; +using System.Collections.Generic; + +namespace PlayniteSounds.Models +{ + public class PlayniteSoundsSettings: ObservableObject + { + public AudioState MusicState { get; set; } = AudioState.Always; + public AudioState SoundState { get; set; } = AudioState.Always; + public MusicType MusicType { get; set; } = MusicType.Game; + public int MusicVolume { get; set; } = 25; + public bool StopMusic { get; set; } = true; + public bool SkipFirstSelectSound { get; set; } + public bool PlayBackupMusic { get; set; } + public bool PauseOnDeactivate { get; set; } = true; + public bool RandomizeOnEverySelect { get; set; } + public bool RandomizeOnMusicEnd { get; set; } = true; + public bool TagMissingEntries { get; set; } + public bool AutoDownload { get; set; } + public bool AutoParallelDownload { get; set; } + public bool ManualParallelDownload { get; set; } = true; + public bool YtPlaylists { get; set; } = true; + public bool NormalizeMusic { get; set; } = true; + public bool TagNormalizedGames { get; set; } + + [DontSerialize] + private string ytDlpPath { get; set; } + public string YtDlpPath { + get => ytDlpPath; + set + { + ytDlpPath = value; + OnPropertyChanged(); + } + } + + [DontSerialize] + private string ffmpegPath { get; set; } + public string FFmpegPath + { + get => ffmpegPath; + set + { + ffmpegPath = value; + OnPropertyChanged(); + } + } + + [DontSerialize] + private string ffmpegNormalizePath { get; set; } + public string FFmpegNormalizePath + { + get => ffmpegNormalizePath; + set + { + ffmpegNormalizePath = value; + OnPropertyChanged(); + } + } + + + public string FFmpegNormalizeArgs { get; set; } + public IList Downloaders { get; set; } = new List { Source.Youtube }; + public DateTime LastAutoLibUpdateAssetsDownload { get; set; } = DateTime.Now; + public bool PromptedForMigration { get; set; } + } +} diff --git a/PlayniteSounds.cs b/PlayniteSounds.cs index fb89e31..2f1c8fb 100644 --- a/PlayniteSounds.cs +++ b/PlayniteSounds.cs @@ -20,6 +20,7 @@ using PlayniteSounds.Common.Constants; using PlayniteSounds.Models; using PlayniteSounds.Controls; +using System.Runtime; namespace PlayniteSounds { @@ -158,9 +159,14 @@ public PlayniteSounds(IPlayniteAPI api) : base(api) { SourceName = "Sounds", ElementList = new List { "MusicControl" } - }); - + }); + #endregion + AddSettingsSupport(new AddSettingsSupportArgs + { + SourceName = "Sounds", + SettingsRoot = $"{nameof(SettingsModel)}.{nameof(SettingsModel.Settings)}" + }); } catch (Exception e) diff --git a/PlayniteSounds.csproj b/PlayniteSounds.csproj index ef77620..712d7f3 100644 --- a/PlayniteSounds.csproj +++ b/PlayniteSounds.csproj @@ -33,12 +33,6 @@ 4 - - packages\AngleSharp.0.17.1\lib\net461\AngleSharp.dll - - - packages\CliWrap.3.4.4\lib\net461\CliWrap.dll - packages\HtmlAgilityPack.1.11.46\lib\Net45\HtmlAgilityPack.dll @@ -83,12 +77,16 @@ packages\System.Text.Encodings.Web.6.0.0\lib\net461\System.Text.Encodings.Web.dll - + packages\System.Text.Json.6.0.5\lib\net461\System.Text.Json.dll + True packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + @@ -97,12 +95,6 @@ - - packages\YoutubeExplode.6.2.2\lib\net461\YoutubeExplode.dll - - - packages\YoutubeExplode.Converter.6.2.2\lib\net461\YoutubeExplode.Converter.dll - @@ -112,6 +104,7 @@ + diff --git a/PlayniteSoundsSettingsView.xaml b/PlayniteSoundsSettingsView.xaml index 6d73e26..2bfb1f0 100644 --- a/PlayniteSoundsSettingsView.xaml +++ b/PlayniteSoundsSettingsView.xaml @@ -132,48 +132,68 @@ - - + + + + + + + +