This commit is contained in:
Tyler 2025-06-19 21:45:43 -04:00 committed by GitHub
commit a179f322cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 257 additions and 0 deletions

View file

@ -5,6 +5,7 @@ namespace NzbDrone.Core.ImportLists
Program,
Spotify,
LastFm,
Youtube,
Other,
Advanced
}

View file

@ -0,0 +1,65 @@
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<TSettings> : ImportListBase<TSettings>
where TSettings : YoutubePlaylistSettings, new()
{
protected YoutubeImportListBase(IImportListStatusService importListStatusService,
IConfigService configService,
IParsingService parsingService,
IHttpClient httpClient,
Logger logger)
: base(importListStatusService, configService, parsingService, logger)
{
}
public override ImportListType ListType => ImportListType.Youtube;
public override TimeSpan MinRefreshInterval => TimeSpan.FromSeconds(1);
public override IList<ImportListItemInfo> Fetch()
{
IList<YoutubeImportListItemInfo> releases = new List<YoutubeImportListItemInfo>();
using (var service = new YouTubeService(new BaseClientService.Initializer()
{
ApiKey = Settings.YoutubeApiKey,
}))
{
releases = Fetch(service);
}
return CleanupListItems(releases);
}
public abstract IList<YoutubeImportListItemInfo> Fetch(YouTubeService service);
protected override void Test(List<ValidationFailure> failures)
{
failures.AddIfNotNull(TestConnection());
}
public abstract ValidationFailure TestConnection(YouTubeService service);
private ValidationFailure TestConnection()
{
using (var service = new YouTubeService(new BaseClientService.Initializer()
{
ApiKey = Settings.YoutubeApiKey,
}))
{
return TestConnection(service);
}
}
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using DryIoc.ImTools;
using FluentValidation.Results;
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<YoutubePlaylistSettings>
{
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<YoutubeImportListItemInfo> Fetch(YouTubeService service)
{
return Settings.PlaylistIds.SelectMany(x => Fetch(service, x)).ToList();
}
public IList<YoutubeImportListItemInfo> Fetch(YouTubeService service, string playlistId)
{
var results = new List<YoutubeImportListItemInfo>();
var req = service.PlaylistItems.List("contentDetails,snippet");
req.PlaylistId = playlistId;
req.MaxResults = 50;
var page = 0;
var playlist = req.Execute();
do
{
page++;
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);
}
Thread.Sleep(TimeSpan.FromSeconds(1));
playlist = req.Execute();
}
while (playlist.NextPageToken != null && page < 10);
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;
}
public override ValidationFailure TestConnection(YouTubeService service)
{
foreach (var id in Settings.PlaylistIds)
{
try
{
var req = service.PlaylistItems.List("contentDetails,snippet");
req.PlaylistId = id;
req.MaxResults = 1;
req.Execute();
}
catch (Exception e)
{
return new ValidationFailure(string.Empty, e.Message);
}
}
return null;
}
}
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
namespace NzbDrone.Core.ImportLists.Youtube
{
public class YoutubePlaylistSettingsValidator : YoutubeSettingsBaseValidator<YoutubePlaylistSettings>
{
public YoutubePlaylistSettingsValidator()
: base()
{
RuleFor(c => c.PlaylistIds).NotEmpty();
}
}
public class YoutubePlaylistSettings : YoutubeSettingsBase<YoutubePlaylistSettings>
{
protected override AbstractValidator<YoutubePlaylistSettings> Validator =>
new YoutubePlaylistSettingsValidator();
public YoutubePlaylistSettings()
{
PlaylistIds = System.Array.Empty<string>();
}
[FieldDefinition(1, Label = "Youtube API key", Type = FieldType.Textbox)]
public string YoutubeApiKey { get; set; }
[FieldDefinition(1, Label = "Playlists", Type = FieldType.Textbox)]
public IEnumerable<string> PlaylistIds { get; set; }
}
}

View file

@ -0,0 +1,23 @@
using FluentValidation;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.ImportLists.Youtube
{
public class YoutubeSettingsBaseValidator<TSettings> : AbstractValidator<TSettings>
where TSettings : YoutubeSettingsBase<TSettings>
{
}
public class YoutubeSettingsBase<TSettings> : IImportListSettings
where TSettings : YoutubeSettingsBase<TSettings>
{
protected virtual AbstractValidator<TSettings> Validator => new YoutubeSettingsBaseValidator<TSettings>();
public string BaseUrl { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate((TSettings)this));
}
}
}

View file

@ -6,6 +6,7 @@
<PackageReference Include="Dapper" Version="2.0.151" />
<PackageReference Include="Diacritical.Net" Version="1.0.4" />
<PackageReference Include="Equ" Version="2.3.0" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.69.0.3680" />
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="Polly" Version="8.5.2" />
<PackageReference Include="System.Text.Json" Version="6.0.10" />