Skip to content

Commit e96bdb3

Browse files
Meynformeo14
authored andcommitted
Implement Youtube only Indexer
1 parent 8772cfa commit e96bdb3

File tree

8 files changed

+474
-33
lines changed

8 files changed

+474
-33
lines changed

Tubifarry/Download/Clients/YouTube/YoutubeClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public override void RemoveItem(DownloadClientItem item, bool deleteData)
5151
protected override void Test(List<ValidationFailure> failures)
5252
{
5353
_dlManager.SetCookies(Settings.CookiePath);
54-
if (Settings.DownloadPath != null)
54+
if (string.IsNullOrEmpty(Settings.DownloadPath))
5555
failures.AddRange(PermissionTester.TestAllPermissions(Settings.FFmpegPath, _logger));
5656
failures.AddIfNotNull(TestFFmpeg().Result);
5757
}
Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,8 @@
1-
using FluentValidation;
2-
using NzbDrone.Core.Annotations;
3-
using NzbDrone.Core.Indexers;
4-
using NzbDrone.Core.Validation;
5-
using Tubifarry.Core.Utilities;
1+
using Tubifarry.Indexers.Youtube;
62

73
namespace Tubifarry.Indexers.Spotify
84
{
9-
public class SpotifyIndexerSettingsValidator : AbstractValidator<SpotifyIndexerSettings>
10-
{
11-
public SpotifyIndexerSettingsValidator()
12-
{
13-
// Validate CookiePath (if provided)
14-
RuleFor(x => x.CookiePath)
15-
.Must(path => string.IsNullOrEmpty(path) || File.Exists(path))
16-
.WithMessage("Cookie file does not exist. Please provide a valid path to the cookies file.")
17-
.Must(path => string.IsNullOrEmpty(path) || CookieManager.ParseCookieFile(path).Any())
18-
.WithMessage("Cookie file is invalid or contains no valid cookies.");
19-
}
20-
}
5+
public class SpotifyIndexerSettingsValidator : YoutubeIndexerSettingsValidator { }
216

22-
public class SpotifyIndexerSettings : IIndexerSettings
23-
{
24-
private static readonly SpotifyIndexerSettingsValidator Validator = new();
25-
26-
[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)]
27-
public int? EarlyReleaseLimit { get; set; } = null;
28-
29-
[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)]
30-
public string CookiePath { get; set; } = string.Empty;
31-
32-
public string BaseUrl { get; set; } = string.Empty;
33-
34-
public NzbDroneValidationResult Validate() => new(Validator.Validate(this));
35-
}
7+
public class SpotifyIndexerSettings : YoutubeIndexerSettings { }
368
}

Tubifarry/Indexers/Spotify/TubifarryIndexer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal class TubifarryIndexer : HttpIndexerBase<SpotifyIndexerSettings>
1313
{
1414
public override string Name => "Tubifarry";
1515
public override string Protocol => nameof(YoutubeDownloadProtocol);
16-
public override bool SupportsRss => false;
16+
public override bool SupportsRss => true;
1717
public override bool SupportsSearch => true;
1818
public override int PageSize => 50;
1919
public override TimeSpan RateLimit => new(3);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using Newtonsoft.Json.Linq;
2+
using YouTubeMusicAPI.Models;
3+
using YouTubeMusicAPI.Types;
4+
5+
namespace Tubifarry.Indexers.Youtube
6+
{
7+
internal static class YoutubeAPISelectors
8+
{
9+
public static T SelectObject<T>(this JToken value, string path)
10+
{
11+
object? result = value.SelectToken(path)?.ToObject(typeof(T));
12+
return result == null ? throw new ArgumentNullException(path, "Required token is null.") : (T)result;
13+
}
14+
15+
public static T? SelectObjectOptional<T>(this JToken value, string path) =>
16+
(T?)value.SelectToken(path)?.ToObject(typeof(T));
17+
18+
public static Radio SelectRadio(this JToken value, string playlistIdPath = "menu.menuRenderer.items[0].menuNavigationItemRenderer.navigationEndpoint.watchEndpoint.playlistId", string? videoIdPath = null) =>
19+
new(value.SelectObject<string>(playlistIdPath), videoIdPath == null ? null : value.SelectObjectOptional<string>(videoIdPath));
20+
21+
public static Thumbnail[] SelectThumbnails(this JToken value, string path = "thumbnail.musicThumbnailRenderer.thumbnail.thumbnails")
22+
{
23+
JToken? thumbnails = value.SelectToken(path);
24+
if (thumbnails == null) return Array.Empty<Thumbnail>();
25+
26+
return thumbnails
27+
.Select(t => new
28+
{
29+
Url = t.SelectToken("url")?.ToString(),
30+
Width = t.SelectToken("width")?.ToString(),
31+
Height = t.SelectToken("height")?.ToString()
32+
})
33+
.Where(t => t.Url != null)
34+
.Select(t => new Thumbnail(t.Url!, int.Parse(t.Width ?? "0"), int.Parse(t.Height ?? "0")))
35+
.ToArray();
36+
}
37+
38+
public static YouTubeMusicItem[] SelectArtists(this JToken value, string path, int startIndex = 0, int trimBy = 0)
39+
{
40+
JToken[] runs = value.SelectObject<JToken[]>(path);
41+
return runs
42+
.Skip(startIndex)
43+
.Take(runs.Length - trimBy - startIndex)
44+
.Select(run => new
45+
{
46+
Artist = run.SelectToken("text")?.ToString()?.Trim(),
47+
ArtistId = run.SelectToken("navigationEndpoint.browseEndpoint.browseId")?.ToString()
48+
})
49+
.Where(a => a.Artist != null && a.Artist != "," && a.Artist != "&" && a.Artist != "•")
50+
.Select(a => new YouTubeMusicItem(a.Artist!, a.ArtistId, YouTubeMusicItemKind.Artists))
51+
.ToArray();
52+
}
53+
}
54+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using FluentValidation.Results;
2+
using NLog;
3+
using NzbDrone.Common.Http;
4+
using NzbDrone.Core.Configuration;
5+
using NzbDrone.Core.Indexers;
6+
using NzbDrone.Core.Parser;
7+
using NzbDrone.Core.ThingiProvider;
8+
using Requests;
9+
using Tubifarry.Indexers.Spotify;
10+
11+
namespace Tubifarry.Indexers.Youtube
12+
{
13+
internal class YoutubeIndexer : HttpIndexerBase<SpotifyIndexerSettings>
14+
{
15+
public override string Name => "Youtube";
16+
public override string Protocol => nameof(YoutubeDownloadProtocol);
17+
public override bool SupportsRss => false;
18+
public override bool SupportsSearch => true;
19+
public override int PageSize => 50;
20+
public override TimeSpan RateLimit => new(30);
21+
22+
private readonly IYoutubeRequestGenerator _indexerRequestGenerator;
23+
24+
private readonly IYoutubeParser _parseIndexerResponse;
25+
26+
public override ProviderMessage Message => new(
27+
"YouTube frequently blocks downloads to prevent unauthorized access. To confirm you're not a bot, you may need to provide additional verification. " +
28+
"This issue can often be partially resolved by using a `cookies.txt` file containing your login tokens. " +
29+
"Ensure the file is properly formatted and includes valid session data to bypass restrictions. " +
30+
"Note: YouTube does not always provide the best metadata for tracks, so you may need to manually verify or update track information.",
31+
ProviderMessageType.Warning
32+
);
33+
34+
public YoutubeIndexer(IYoutubeParser parser, IYoutubeRequestGenerator generator, IHttpClient httpClient, IIndexerStatusService indexerStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
35+
: base(httpClient, indexerStatusService, configService, parsingService, logger)
36+
{
37+
_parseIndexerResponse = parser;
38+
_indexerRequestGenerator = generator;
39+
40+
RequestHandler.MainRequestHandlers[0].MaxParallelism = 2;
41+
}
42+
43+
protected override Task Test(List<ValidationFailure> failures)
44+
{
45+
_parseIndexerResponse.SetCookies(Settings.CookiePath);
46+
_indexerRequestGenerator.SetCookies(Settings.CookiePath);
47+
return Task.CompletedTask;
48+
}
49+
50+
public override IIndexerRequestGenerator GetRequestGenerator() => _indexerRequestGenerator;
51+
52+
public override IParseIndexerResponse GetParser() => _parseIndexerResponse;
53+
}
54+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+

2+
using FluentValidation;
3+
using NzbDrone.Core.Annotations;
4+
using NzbDrone.Core.Indexers;
5+
using NzbDrone.Core.Validation;
6+
using Tubifarry.Core.Utilities;
7+
8+
namespace Tubifarry.Indexers.Youtube
9+
{
10+
11+
public class YoutubeIndexerSettingsValidator : AbstractValidator<YoutubeIndexerSettings>
12+
{
13+
public YoutubeIndexerSettingsValidator()
14+
{
15+
// Validate CookiePath (if provided)
16+
RuleFor(x => x.CookiePath)
17+
.Must(path => string.IsNullOrEmpty(path) || File.Exists(path))
18+
.WithMessage("Cookie file does not exist. Please provide a valid path to the cookies file.")
19+
.Must(path => string.IsNullOrEmpty(path) || CookieManager.ParseCookieFile(path).Any())
20+
.WithMessage("Cookie file is invalid or contains no valid cookies.");
21+
}
22+
}
23+
24+
public class YoutubeIndexerSettings : IIndexerSettings
25+
{
26+
private static readonly YoutubeIndexerSettingsValidator Validator = new();
27+
28+
[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)]
29+
public int? EarlyReleaseLimit { get; set; } = null;
30+
31+
[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)]
32+
public string CookiePath { get; set; } = string.Empty;
33+
34+
public string BaseUrl { get; set; } = string.Empty;
35+
36+
public NzbDroneValidationResult Validate() => new(Validator.Validate(this));
37+
}
38+
}

0 commit comments

Comments
 (0)