Skip to content

Commit

Permalink
Bump YouTube Music API to latest version (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
Meyn committed Feb 8, 2025
1 parent d6310ee commit 8e43da0
Show file tree
Hide file tree
Showing 12 changed files with 80 additions and 30 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Tubifarry supports **Slskd**, the Soulseek client, as both an **indexer** and **

### YouTube Downloader Setup 🎥
> #### YouTube Warning ⚠️
> Please be aware that YouTube often blocks Tubifarry as a bot. We are currently waiting for external updates. Logging in and the YouTube-only indexer are disabled for now. If login is necessary, please revert to versions earlier than 1.6.0. We appreciate your patience and understanding during this time.
> YouTube may restrict access to Tubifarry, as it is identified as a bot. We appreciate your understanding and patience in this matter.

Tubifarry allows you to download music directly from YouTube. Follow the steps below to configure the YouTube downloader.

Expand Down
5 changes: 2 additions & 3 deletions Tubifarry/Download/Clients/Soulseek/SlskdClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public override async Task<string> Download(RemoteAlbum remoteAlbum, IIndexer in
{
RemoveItemAsync(item).Wait();
}
catch (Exception) { }
catch { }
return null!;
}
return item.ID.ToString();
Expand Down Expand Up @@ -152,8 +152,7 @@ private async Task UpdateDownloadItemsAsync()
downloads?.ForEach(user =>
{
user.TryGetProperty("directories", out JsonElement directoriesElement);
IEnumerable<SlskdDownloadDirectory> data = SlskdDownloadDirectory.GetDirectories(directoriesElement);
foreach (SlskdDownloadDirectory dir in data)
foreach (SlskdDownloadDirectory dir in SlskdDownloadDirectory.GetDirectories(directoriesElement))
{
HashCode hash = new();
List<string> sortedFilenames = dir.Files?
Expand Down
11 changes: 10 additions & 1 deletion Tubifarry/Download/Clients/YouTube/YouTubeAlbumOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ internal record YouTubeAlbumOptions : RequestOptions<string, string>

public NamingConfig? NameingConfig { get; set; }

public int RandomDelayMin { get; set; } = 100;

public int RandomDelayMax { get; set; } = 2000;

public int MaxDownloadSpeed { get; set; }

public YouTubeAlbumOptions() { }

protected YouTubeAlbumOptions(YouTubeAlbumOptions options) : base(options)
Expand All @@ -41,6 +47,9 @@ protected YouTubeAlbumOptions(YouTubeAlbumOptions options) : base(options)
ReEncodeOptions = options.ReEncodeOptions;
LRCLIBInstance = options.LRCLIBInstance;
DownloadPath = options.DownloadPath;
RandomDelayMin = options.RandomDelayMin;
RandomDelayMax = options.RandomDelayMax;
MaxDownloadSpeed = options.MaxDownloadSpeed;
}
}
}
}
24 changes: 16 additions & 8 deletions Tubifarry/Download/Clients/YouTube/YouTubeAlbumRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ internal class YouTubeAlbumRequest : Request<YouTubeAlbumOptions, string, string
private readonly Logger _logger;

private DateTime _lastUpdateTime = DateTime.MinValue;
private long _lastRemainingSize = 0;
private long _lastRemainingSize;
private byte[]? _albumCover;

private ReleaseInfo ReleaseInfo => _remoteAlbum.Release;
Expand Down Expand Up @@ -88,6 +88,7 @@ private async Task ProcessAlbumAsync(CancellationToken token)
{
string albumBrowseID = await Options.YouTubeMusicClient!.GetAlbumBrowseIdAsync(ReleaseInfo.DownloadUrl, token).ConfigureAwait(false);
AlbumInfo albumInfo = await Options.YouTubeMusicClient.GetAlbumInfoAsync(albumBrowseID, token).ConfigureAwait(false);
await ApplyRandomDelayAsync(token);
if (albumInfo?.Songs == null || !albumInfo.Songs.Any())
{
LogAndAppendMessage($"No tracks to download found in the album: {ReleaseInfo.Album}", LogLevel.Debug);
Expand Down Expand Up @@ -157,16 +158,14 @@ private void AddTrackDownloadRequests(AlbumInfo albumInfo, AlbumSongInfo trackIn
CreateSpeedReporter = true,
SpeedReporterTimeout = 1,
Priority = RequestPriority.Normal,
MaxBytesPerSecond = Options.MaxDownloadSpeed,
DelayBetweenAttemps = Options.DelayBetweenAttemps,
Filename = _releaseFormatter.BuildTrackFilename(null, musicInfo, _albumData) + ".m4a",
DestinationPath = _destinationPath.FullPath,
Handler = Options.Handler,
DeleteFilesOnFailure = true,
Chunks = Options.Chunks,
RequestFailed = (req, path) =>
{
LogAndAppendMessage($"Downloading track '{trackInfo.Name}' in album '{albumInfo.Name}' failed.", LogLevel.Debug);
},
RequestFailed = (_, __) => LogAndAppendMessage($"Downloading track '{trackInfo.Name}' in album '{albumInfo.Name}' failed.", LogLevel.Debug),
WriteMode = WriteMode.AppendOrTruncate,
});

Expand All @@ -176,15 +175,15 @@ private void AddTrackDownloadRequests(AlbumInfo albumInfo, AlbumSongInfo trackIn
Priority = RequestPriority.High,
DelayBetweenAttemps = Options.DelayBetweenAttemps,
Handler = Options.Handler,
RequestFailed = (req, path) =>
RequestFailed = (_, __) =>
{
LogAndAppendMessage($"Post-processing for track '{trackInfo.Name}' in album '{albumInfo.Name}' failed.", LogLevel.Debug);
try
{
if (File.Exists(downloadingReq.Destination))
File.Delete(downloadingReq.Destination);
}
catch (Exception) { }
catch { }
},
CancellationToken = Token
});
Expand Down Expand Up @@ -267,7 +266,16 @@ private string GetDistinctMessages()
ReadOnlyMemory<char> chunk = chunkEnumerator.Current;
distinctMessages.Add(chunk.ToString());
}
return string.Join("", distinctMessages);
return string.Concat(distinctMessages);
}

private async Task ApplyRandomDelayAsync(CancellationToken token)
{
if (Options.RandomDelayMin > 0 && Options.RandomDelayMax > 0)
{
int delay = new Random().Next(Options.RandomDelayMin, Options.RandomDelayMax);
await Task.Delay(delay, token).ConfigureAwait(false);
}
}

private long GetRemainingSize() => Math.Max(_trackContainer.Sum(x => x.ContentLength), ReleaseInfo.Size) - _trackContainer.Sum(x => x.BytesDownloaded);
Expand Down
3 changes: 3 additions & 0 deletions Tubifarry/Download/Clients/YouTube/YoutubeDownloadManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public Task<string> Download(RemoteAlbum remoteAlbum, IIndexer indexer, NamingCo
Chunks = provider.Settings.Chunks,
DelayBetweenAttemps = TimeSpan.FromSeconds(5),
NumberOfAttempts = 2,
RandomDelayMin = provider.Settings.RandomDelayMin,
RandomDelayMax = provider.Settings.RandomDelayMax,
MaxDownloadSpeed = provider.Settings.MaxDownloadSpeed * 1024,
NameingConfig = namingConfig,
LRCLIBInstance = provider.Settings.LRCLIBInstance,
UseID3v2_3 = provider.Settings.UseID3v2_3,
Expand Down
39 changes: 33 additions & 6 deletions Tubifarry/Download/Clients/YouTube/YoutubeProviderSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ public YoutubeProviderSettingsValidator()
.IsValidPath()
.When(x => x.ReEncode != (int)ReEncodeOptions.Disabled)
.WithMessage("Invalid FFmpeg path. Please provide a valid path to the FFmpeg binary.");

// Validate Random Delay Range
RuleFor(x => x.RandomDelayMin)
.LessThanOrEqualTo(x => x.RandomDelayMax)
.WithMessage("Minimum delay must be less than or equal to maximum delay.");

RuleFor(x => x.RandomDelayMax)
.GreaterThanOrEqualTo(x => x.RandomDelayMin)
.WithMessage("Maximum delay must be greater than or equal to minimum delay.")
.GreaterThanOrEqualTo(_ => 0)
.WithMessage("Maximum delay must be greater than or equal to 0.");

// Validate Max Download Speed
RuleFor(x => x.MaxDownloadSpeed)
.GreaterThanOrEqualTo(0)
.WithMessage("Max download speed must be greater than or equal to 0.")
.LessThanOrEqualTo(2_500)
.WithMessage("Max download speed must be less than or equal to 20 Mbps (2,500 KB/s).");
}
}

Expand All @@ -58,7 +76,7 @@ public class YoutubeProviderSettings : IProviderConfig
[FieldDefinition(0, Label = "Download Path", Type = FieldType.Path, HelpText = "Specify the directory where downloaded files will be saved.")]
public string DownloadPath { get; set; } = "";

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

[FieldDefinition(2, Label = "Use ID3v2.3 Tags", HelpText = "Enable this option to use ID3v2.3 tags for better compatibility with older media players like Windows Media Player.", Type = FieldType.Checkbox, Advanced = true)]
Expand All @@ -73,15 +91,24 @@ public class YoutubeProviderSettings : IProviderConfig
[FieldDefinition(5, Label = "LRC Lib Instance", Type = FieldType.Url, HelpText = "The URL of a LRC Lib instance to connect to. Default is 'https://lrclib.net'.", Advanced = true)]
public string LRCLIBInstance { get; set; } = "https://lrclib.net";

[FieldDefinition(6, Label = "File Chunk Count", Type = FieldType.Number, HelpText = "Number of chunks to split the download into. Each chunk is its own download. Note: Non-chunked downloads from YouTube are typically much slower.", Advanced = true)]
public int Chunks { get; set; } = 2;

[FieldDefinition(7, Label = "ReEncode", Type = FieldType.Select, SelectOptions = typeof(ReEncodeOptions), HelpText = "Specify whether to re-encode audio files and how to handle FFmpeg.", Advanced = true)]
[FieldDefinition(6, Label = "ReEncode", Type = FieldType.Select, SelectOptions = typeof(ReEncodeOptions), HelpText = "Specify whether to re-encode audio files and how to handle FFmpeg.", Advanced = true)]
public int ReEncode { get; set; } = (int)ReEncodeOptions.Disabled;

[FieldDefinition(8, Label = "FFmpeg Path", Type = FieldType.Path, Placeholder = "/downloads/FFmpeg", HelpText = "Specify the path to the FFmpeg binary. Not required if 'Disabled' is selected.", Advanced = true)]
[FieldDefinition(7, Label = "FFmpeg Path", Type = FieldType.Path, Placeholder = "/downloads/FFmpeg", HelpText = "Specify the path to the FFmpeg binary. Not required if 'Disabled' is selected.", Advanced = true)]
public string FFmpegPath { get; set; } = string.Empty;

[FieldDefinition(8, Label = "File Chunk Count", Type = FieldType.Number, HelpText = "Number of chunks to split the download into. Each chunk is its own download. Note: Non-chunked downloads from YouTube are typically much slower.", Advanced = true)]
public int Chunks { get; set; } = 2;

[FieldDefinition(9, Label = "Delay Min", Type = FieldType.Number, HelpText = "Minimum random delay between requests to avoid bot notifications.", Unit = "ms", Advanced = true)]
public int RandomDelayMin { get; set; } = 100;

[FieldDefinition(10, Label = "Delay Max", Type = FieldType.Number, HelpText = "Maximum random delay between requests to avoid bot notifications.", Unit = "ms", Advanced = true)]
public int RandomDelayMax { get; set; } = 2000;

[FieldDefinition(11, Label = "Max Download Speed", Type = FieldType.Number, HelpText = "Set to 0 for unlimited speed. Limits the download speed per download.", Unit = "KB/s", Advanced = true)]
public int MaxDownloadSpeed { get; set; }

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

Expand Down
2 changes: 2 additions & 0 deletions Tubifarry/ILRepack.targets
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<InputAssemblies Include="$(OutputPath)Xabe.FFmpeg.Downloader.dll" />
<InputAssemblies Include="$(OutputPath)FuzzySharp.dll" />
<InputAssemblies Include="$(OutputPath)Microsoft.Extensions.Logging.Abstractions.dll" />
<InputAssemblies Include="$(OutputPath)Acornima.dll" />
<InputAssemblies Include="$(OutputPath)Jint.dll" />
</ItemGroup>

<ILRepack
Expand Down
2 changes: 1 addition & 1 deletion Tubifarry/Indexers/Youtube/YoutubeIndexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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 => false;
public override bool SupportsSearch => true;
public override int PageSize => 50;
public override TimeSpan RateLimit => new(30);

Expand Down
2 changes: 1 addition & 1 deletion Tubifarry/Indexers/Youtube/YoutubeIndexerSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class YoutubeIndexerSettings : IIndexerSettings
[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, Hidden = HiddenType.HiddenIfNotSet, 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)]
[FieldDefinition(1, Label = "Cookie Path", Type = FieldType.FilePath, Hidden = HiddenType.Visible, 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;
Expand Down
3 changes: 1 addition & 2 deletions Tubifarry/Indexers/Youtube/YoutubeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

namespace Tubifarry.Indexers.Youtube
{

public interface IYoutubeParser : IParseIndexerResponse
{
public void SetCookies(string path);
Expand Down Expand Up @@ -73,7 +72,7 @@ IEnumerable<Shelf> ParseSearchResponse(JObject requestResponse)
?.Where(token => token["musicShelfRenderer"] is not null)
?.Select(token => token.First!);

if (shelvesData is null || !shelvesData.Any())
if (shelvesData?.Any() != true)
{
_logger?.Warn($"Parsing search failed. Request response does not contain any shelves.");
return new List<Shelf>();
Expand Down
15 changes: 9 additions & 6 deletions Tubifarry/Indexers/Youtube/YoutubeRequestGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ internal class YoutubeRequestGenerator : IYoutubeRequestGenerator

public YoutubeRequestGenerator(Logger logger) => _logger = logger;


public IndexerPageableRequestChain GetRecentRequests()
{
IndexerPageableRequestChain pageableRequests = new();
Expand All @@ -33,8 +32,8 @@ public IndexerPageableRequestChain GetRecentRequests()

private IEnumerable<IndexerRequest> GetRecentReleaseRequests()
{
Dictionary<string, object> payload = Payload.Web(
geographicalLocation: "US",
Dictionary<string, object> payload = Payload.WebRemix(
geographicalLocation: "US", null, null, null,
items: new (string key, object? value)[]
{
("browseId", "FEmusic_new_releases"),
Expand Down Expand Up @@ -85,22 +84,26 @@ public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCriteria search

private IEnumerable<IndexerRequest> GetRequests(string searchQuery, YouTubeMusicItemKind kind)
{
Dictionary<string, object> payload = Payload.Web("US", new (string key, object? value)[] { ("query", searchQuery), ("params", Extensions.ToParams(kind)), ("continuation", null) });
Dictionary<string, object> payload = Payload.WebRemix("US", null, null, null, new (string key, object? value)[] { ("query", searchQuery), ("params", Extensions.ToParams(kind)), ("continuation", null) });

string jsonPayload = JsonConvert.SerializeObject(payload, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });

string url = $"https://music.youtube.com/youtubei/v1/search";
HttpRequest s = new(url, HttpAccept.Json);
if (!string.IsNullOrEmpty(_cookiePath))
{
foreach (System.Net.Cookie cookie in CookieManager.ParseCookieFile(_cookiePath))
{
if (s.Cookies.ContainsKey(cookie.Name))
s.Cookies[cookie.Name] = cookie.Value;
else
s.Cookies.Add(cookie.Name, cookie.Value);
}
}

s.Method = HttpMethod.Post;
s.SetContent(jsonPayload);
IndexerRequest req = new(s);
yield return req;
yield return new(s);
}
}
}
2 changes: 1 addition & 1 deletion Tubifarry/Tubifarry.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<PackageReference Include="Shard.DownloadAssistant" Version="1.1.0" />
<PackageReference Include="Xabe.FFmpeg" Version="5.2.6" />
<PackageReference Include="Xabe.FFmpeg.Downloader" Version="5.2.6" />
<PackageReference Include="YouTubeMusicAPI" Version="2.2.0" />
<PackageReference Include="YouTubeMusicAPI" Version="2.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit 8e43da0

Please sign in to comment.