Skip to content

Commit

Permalink
Implement Youtube only Indexer
Browse files Browse the repository at this point in the history
  • Loading branch information
Meyn committed Jan 18, 2025
1 parent 8772cfa commit 8e8faf6
Show file tree
Hide file tree
Showing 8 changed files with 474 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Tubifarry/Download/Clients/YouTube/YoutubeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public override void RemoveItem(DownloadClientItem item, bool deleteData)
protected override void Test(List<ValidationFailure> failures)
{
_dlManager.SetCookies(Settings.CookiePath);
if (Settings.DownloadPath != null)
if (string.IsNullOrEmpty(Settings.DownloadPath))
failures.AddRange(PermissionTester.TestAllPermissions(Settings.FFmpegPath, _logger));
failures.AddIfNotNull(TestFFmpeg().Result);
}
Expand Down
34 changes: 3 additions & 31 deletions Tubifarry/Indexers/Spotify/SpotifySettings.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,8 @@
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation;
using Tubifarry.Core.Utilities;
using Tubifarry.Indexers.Youtube;

namespace Tubifarry.Indexers.Spotify
{
public class SpotifyIndexerSettingsValidator : AbstractValidator<SpotifyIndexerSettings>
{
public SpotifyIndexerSettingsValidator()
{
// Validate CookiePath (if provided)
RuleFor(x => x.CookiePath)
.Must(path => string.IsNullOrEmpty(path) || File.Exists(path))
.WithMessage("Cookie file does not exist. Please provide a valid path to the cookies file.")
.Must(path => string.IsNullOrEmpty(path) || CookieManager.ParseCookieFile(path).Any())
.WithMessage("Cookie file is invalid or contains no valid cookies.");
}
}
public class SpotifyIndexerSettingsValidator : YoutubeIndexerSettingsValidator { }

public class SpotifyIndexerSettings : IIndexerSettings
{
private static readonly SpotifyIndexerSettingsValidator Validator = new();

[FieldDefinition(0, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)]
public int? EarlyReleaseLimit { get; set; } = null;

[FieldDefinition(1, Label = "Cookie Path", Type = FieldType.FilePath, Placeholder = "/path/to/cookies.txt", HelpText = "Specify the path to the Spotify cookies file. This is optional but required for accessing restricted content.", Advanced = true)]
public string CookiePath { get; set; } = string.Empty;

public string BaseUrl { get; set; } = string.Empty;

public NzbDroneValidationResult Validate() => new(Validator.Validate(this));
}
public class SpotifyIndexerSettings : YoutubeIndexerSettings { }
}
2 changes: 1 addition & 1 deletion Tubifarry/Indexers/Spotify/TubifarryIndexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ internal class TubifarryIndexer : HttpIndexerBase<SpotifyIndexerSettings>
{
public override string Name => "Tubifarry";
public override string Protocol => nameof(YoutubeDownloadProtocol);
public override bool SupportsRss => false;
public override bool SupportsRss => true;
public override bool SupportsSearch => true;
public override int PageSize => 50;
public override TimeSpan RateLimit => new(3);
Expand Down
54 changes: 54 additions & 0 deletions Tubifarry/Indexers/Youtube/YoutubeAPISelectors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Newtonsoft.Json.Linq;
using YouTubeMusicAPI.Models;
using YouTubeMusicAPI.Types;

namespace Tubifarry.Indexers.Youtube
{
internal static class YoutubeAPISelectors
{
public static T SelectObject<T>(this JToken value, string path)
{
object? result = value.SelectToken(path)?.ToObject(typeof(T));
return result == null ? throw new ArgumentNullException(path, "Required token is null.") : (T)result;
}

public static T? SelectObjectOptional<T>(this JToken value, string path) =>
(T?)value.SelectToken(path)?.ToObject(typeof(T));

public static Radio SelectRadio(this JToken value, string playlistIdPath = "menu.menuRenderer.items[0].menuNavigationItemRenderer.navigationEndpoint.watchEndpoint.playlistId", string? videoIdPath = null) =>
new(value.SelectObject<string>(playlistIdPath), videoIdPath == null ? null : value.SelectObjectOptional<string>(videoIdPath));

public static Thumbnail[] SelectThumbnails(this JToken value, string path = "thumbnail.musicThumbnailRenderer.thumbnail.thumbnails")
{
JToken? thumbnails = value.SelectToken(path);
if (thumbnails == null) return Array.Empty<Thumbnail>();

return thumbnails
.Select(t => new
{
Url = t.SelectToken("url")?.ToString(),
Width = t.SelectToken("width")?.ToString(),
Height = t.SelectToken("height")?.ToString()
})
.Where(t => t.Url != null)
.Select(t => new Thumbnail(t.Url!, int.Parse(t.Width ?? "0"), int.Parse(t.Height ?? "0")))
.ToArray();
}

public static YouTubeMusicItem[] SelectArtists(this JToken value, string path, int startIndex = 0, int trimBy = 0)
{
JToken[] runs = value.SelectObject<JToken[]>(path);
return runs
.Skip(startIndex)
.Take(runs.Length - trimBy - startIndex)
.Select(run => new
{
Artist = run.SelectToken("text")?.ToString()?.Trim(),
ArtistId = run.SelectToken("navigationEndpoint.browseEndpoint.browseId")?.ToString()
})
.Where(a => a.Artist != null && a.Artist != "," && a.Artist != "&" && a.Artist != "•")
.Select(a => new YouTubeMusicItem(a.Artist!, a.ArtistId, YouTubeMusicItemKind.Artists))
.ToArray();
}
}
}
54 changes: 54 additions & 0 deletions Tubifarry/Indexers/Youtube/YoutubeIndexer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser;
using NzbDrone.Core.ThingiProvider;
using Requests;
using Tubifarry.Indexers.Spotify;

namespace Tubifarry.Indexers.Youtube
{
internal class YoutubeIndexer : HttpIndexerBase<SpotifyIndexerSettings>
{
public override string Name => "Youtube";
public override string Protocol => nameof(YoutubeDownloadProtocol);
public override bool SupportsRss => false;
public override bool SupportsSearch => true;
public override int PageSize => 50;
public override TimeSpan RateLimit => new(30);

private readonly IYoutubeRequestGenerator _indexerRequestGenerator;

private readonly IYoutubeParser _parseIndexerResponse;

public override ProviderMessage Message => new(
"YouTube frequently blocks downloads to prevent unauthorized access. To confirm you're not a bot, you may need to provide additional verification. " +
"This issue can often be partially resolved by using a `cookies.txt` file containing your login tokens. " +
"Ensure the file is properly formatted and includes valid session data to bypass restrictions. " +
"Note: YouTube does not always provide the best metadata for tracks, so you may need to manually verify or update track information.",
ProviderMessageType.Warning
);

public YoutubeIndexer(IYoutubeParser parser, IYoutubeRequestGenerator generator, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
: base(httpClient, indexerStatusService, configService, parsingService, logger)
{
_parseIndexerResponse = parser;
_indexerRequestGenerator = generator;

RequestHandler.MainRequestHandlers[0].MaxParallelism = 2;
}

protected override Task Test(List<ValidationFailure> failures)
{
_parseIndexerResponse.SetCookies(Settings.CookiePath);
_indexerRequestGenerator.SetCookies(Settings.CookiePath);
return Task.CompletedTask;
}

public override IIndexerRequestGenerator GetRequestGenerator() => _indexerRequestGenerator;

public override IParseIndexerResponse GetParser() => _parseIndexerResponse;
}
}
38 changes: 38 additions & 0 deletions Tubifarry/Indexers/Youtube/YoutubeIndexerSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@

using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation;
using Tubifarry.Core.Utilities;

namespace Tubifarry.Indexers.Youtube
{

public class YoutubeIndexerSettingsValidator : AbstractValidator<YoutubeIndexerSettings>
{
public YoutubeIndexerSettingsValidator()
{
// Validate CookiePath (if provided)
RuleFor(x => x.CookiePath)
.Must(path => string.IsNullOrEmpty(path) || File.Exists(path))
.WithMessage("Cookie file does not exist. Please provide a valid path to the cookies file.")
.Must(path => string.IsNullOrEmpty(path) || CookieManager.ParseCookieFile(path).Any())
.WithMessage("Cookie file is invalid or contains no valid cookies.");
}
}

public class YoutubeIndexerSettings : IIndexerSettings
{
private static readonly YoutubeIndexerSettingsValidator Validator = new();

[FieldDefinition(0, Type = FieldType.Number, Label = "Early Download Limit", Unit = "days", HelpText = "Time before release date Lidarr will download from this indexer, empty is no limit", Advanced = true)]
public int? EarlyReleaseLimit { get; set; } = null;

[FieldDefinition(1, Label = "Cookie Path", Type = FieldType.FilePath, Placeholder = "/path/to/cookies.txt", HelpText = "Specify the path to the Spotify cookies file. This is optional but required for accessing restricted content.", Advanced = true)]
public string CookiePath { get; set; } = string.Empty;

public string BaseUrl { get; set; } = string.Empty;

public NzbDroneValidationResult Validate() => new(Validator.Validate(this));
}
}
Loading

0 comments on commit 8e8faf6

Please sign in to comment.