diff --git a/src/NzbDrone.Core/ImportLists/ImportListType.cs b/src/NzbDrone.Core/ImportLists/ImportListType.cs index 6dd76ef31..d3be3b655 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListType.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListType.cs @@ -5,6 +5,7 @@ namespace NzbDrone.Core.ImportLists Program, Spotify, LastFm, + Youtube, Other, Advanced } diff --git a/src/NzbDrone.Core/ImportLists/Youtube/YoutubeImportListBase.cs b/src/NzbDrone.Core/ImportLists/Youtube/YoutubeImportListBase.cs new file mode 100644 index 000000000..b0d46bb2c --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Youtube/YoutubeImportListBase.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using FluentValidation.Results; +using Google.Apis.Services; +using Google.Apis.YouTube.v3; +using NLog; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.Youtube +{ + public abstract class YoutubeImportListBase : ImportListBase + where TSettings : YoutubeSettingsBase, new() + { + private IHttpClient _httpClient; + + protected YoutubeImportListBase(IImportListStatusService importListStatusService, + IConfigService configService, + IParsingService parsingService, + IHttpClient httpClient, + Logger logger) + : base(importListStatusService, configService, parsingService, logger) + { + _httpClient = httpClient; + } + + public override ImportListType ListType => ImportListType.Youtube; + public override TimeSpan MinRefreshInterval => TimeSpan.FromSeconds(1); + + public override IList Fetch() + { + IList releases = new List(); + + using (var service = new YouTubeService(new BaseClientService.Initializer() + { + ApiKey = "" + })) + { + releases = Fetch(service); + } + + // TODO remap + + return CleanupListItems(releases); + } + + public IList MapYoutubeReleases(IList items) + { + return items; + } + + public abstract IList Fetch(YouTubeService service); + + protected override void Test(List failures) + { + failures.AddIfNotNull(TestConnection()); + } + + private ValidationFailure TestConnection() + { + return null; + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Youtube/YoutubeImportListItemInfo.cs b/src/NzbDrone.Core/ImportLists/Youtube/YoutubeImportListItemInfo.cs new file mode 100644 index 000000000..6c75bc5e6 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Youtube/YoutubeImportListItemInfo.cs @@ -0,0 +1,14 @@ +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.ImportLists.Youtube; + +public class YoutubeImportListItemInfo : ImportListItemInfo +{ + public string ArtistYoutubeId { get; set; } + public string AlbumYoutubeId { get; set; } + + public override string ToString() + { + return string.Format("[{0}] {1}", ArtistYoutubeId, AlbumYoutubeId); + } +} diff --git a/src/NzbDrone.Core/ImportLists/Youtube/YoutubePlaylist.cs b/src/NzbDrone.Core/ImportLists/Youtube/YoutubePlaylist.cs new file mode 100644 index 000000000..476ffc267 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Youtube/YoutubePlaylist.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DryIoc.ImTools; +using Google.Apis.YouTube.v3; +using Google.Apis.YouTube.v3.Data; +using NLog; +using NzbDrone.Common.Http; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Parser; + +namespace NzbDrone.Core.ImportLists.Youtube +{ + public class YoutubePlaylist : YoutubeImportListBase + { + public YoutubePlaylist(IImportListStatusService importListStatusService, + IImportListRepository importListRepository, + IConfigService configService, + IParsingService parsingService, + IHttpClient httpClient, + Logger logger) + : base(importListStatusService, configService, parsingService, httpClient, logger) + { + } + + public override string Name => "Youtube Playlists"; + + public override IList Fetch(YouTubeService service) + { + return Settings.PlaylistIds.SelectMany(x => Fetch(service, x)).ToList(); + } + + public IList Fetch(YouTubeService service, string playlistId) + { + // TODO playlist + var results = new List(); + var req = service.PlaylistItems.List("contentDetails,snippet"); + req.PlaylistId = playlistId; + req.MaxResults = 50; + + while (true) + { + var playlist = req.Execute(); + req.PageToken = playlist.NextPageToken; + + foreach (var song in playlist.Items) + { + var listItem = new YoutubeImportListItemInfo(); + var topicChannel = song.Snippet.VideoOwnerChannelTitle.EndsWith("- Topic"); + if (topicChannel) + { + ParseTopicChannel(song, ref listItem); + } + else + { + // No album name just video + listItem.ReleaseDate = ParseDateTimeOffset(song); + listItem.Artist = song.Snippet.VideoOwnerChannelTitle; + } + + results.Add(listItem); + } + + if (playlist.NextPageToken == null) + { + break; + } + } + + return results; + } + + public void ParseTopicChannel(PlaylistItem playlistItem, ref YoutubeImportListItemInfo listItem) + { + var description = playlistItem.Snippet.Description; + var descArgs = description.Split("\n\n"); + + listItem.Artist = playlistItem.Snippet.VideoOwnerChannelTitle.Contains("- Topic") ? + playlistItem.Snippet.VideoOwnerChannelTitle[.. (playlistItem.Snippet.VideoOwnerChannelTitle.LastIndexOf('-') - 1)] : + playlistItem.Snippet.VideoOwnerChannelTitle; + listItem.Album = descArgs[2]; + + if (descArgs.Any(s => s.StartsWith("Released on:"))) + { + // Custom release date + var release = descArgs.FindFirst(s => s.StartsWith("Released on:")); + var date = release.Substring(release.IndexOf(':') + 1); + listItem.ReleaseDate = DateTime.Parse(date); + } + else + { + listItem.ReleaseDate = ParseDateTimeOffset(playlistItem); + } + } + + private DateTime ParseDateTimeOffset(PlaylistItem playlistItem) + { + return (playlistItem.ContentDetails.VideoPublishedAtDateTimeOffset ?? DateTimeOffset.UnixEpoch).DateTime; + } + + private DateTime ParseYoutubeDate(string date, PlaylistItem song) + { + return DateTime.Now; + } + + public override object RequestAction(string action, IDictionary query) + { + Console.Out.WriteLine(action); + + return base.RequestAction(action, query); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Youtube/YoutubePlaylistSettings.cs b/src/NzbDrone.Core/ImportLists/Youtube/YoutubePlaylistSettings.cs new file mode 100644 index 000000000..9e3a4fb51 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Youtube/YoutubePlaylistSettings.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.Annotations; + +namespace NzbDrone.Core.ImportLists.Youtube +{ + public class YoutubePlaylistSettingsValidator : YoutubeSettingsBaseValidator + { + public YoutubePlaylistSettingsValidator() + : base() + { + RuleFor(c => c.PlaylistIds).NotEmpty(); + } + } + + public class YoutubePlaylistSettings : YoutubeSettingsBase + { + protected override AbstractValidator Validator => + new YoutubePlaylistSettingsValidator(); + + public YoutubePlaylistSettings() + { + PlaylistIds = System.Array.Empty(); + } + + // public override string Scope => "playlist-read-private"; + + [FieldDefinition(1, Label = "Playlists", Type = FieldType.Textbox)] + public IEnumerable PlaylistIds { get; set; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Youtube/YoutubeSettingsBase.cs b/src/NzbDrone.Core/ImportLists/Youtube/YoutubeSettingsBase.cs new file mode 100644 index 000000000..ee8d03d4a --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Youtube/YoutubeSettingsBase.cs @@ -0,0 +1,48 @@ +using System; +using FluentValidation; +using NzbDrone.Core.Annotations; +using NzbDrone.Core.Validation; + +namespace NzbDrone.Core.ImportLists.Youtube +{ + public class YoutubeSettingsBaseValidator : AbstractValidator + where TSettings : YoutubeSettingsBase + { + public YoutubeSettingsBaseValidator() + { + // TODO + } + } + + public class YoutubeSettingsBase : IImportListSettings + where TSettings : YoutubeSettingsBase + { + protected virtual AbstractValidator Validator => new YoutubeSettingsBaseValidator(); + + public YoutubeSettingsBase() + { + BaseUrl = "todo"; + } + + public string BaseUrl { get; set; } + + public virtual string Scope => ""; + + [FieldDefinition(0, Label = "Access Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string AccessToken { get; set; } + + [FieldDefinition(0, Label = "Refresh Token", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public string RefreshToken { get; set; } + + [FieldDefinition(0, Label = "Expires", Type = FieldType.Textbox, Hidden = HiddenType.Hidden)] + public DateTime Expires { get; set; } + + // [FieldDefinition(99, Label = "Authenticate with Google", Type = FieldType.OAuth)] + // public string SignIn { get; set; } + + public NzbDroneValidationResult Validate() + { + return new NzbDroneValidationResult(Validator.Validate((TSettings)this)); + } + } +} diff --git a/src/NzbDrone.Core/Lidarr.Core.csproj b/src/NzbDrone.Core/Lidarr.Core.csproj index a56bfb295..bcf4e45b7 100644 --- a/src/NzbDrone.Core/Lidarr.Core.csproj +++ b/src/NzbDrone.Core/Lidarr.Core.csproj @@ -6,6 +6,7 @@ +