diff --git a/README.md b/README.md index 185bc5467..b7cc35f63 100644 --- a/README.md +++ b/README.md @@ -391,6 +391,7 @@ Developer note: The software implements the [Torznab](https://github.com/Sonarr/ * LeagueHD * LearnFlakes * LegacyHD (HD4Free) + * Libble * LibraNet (LN) * LinkoManija * LosslessClub diff --git a/src/Jackett.Common/Indexers/Libble.cs b/src/Jackett.Common/Indexers/Libble.cs new file mode 100644 index 000000000..e0ab83694 --- /dev/null +++ b/src/Jackett.Common/Indexers/Libble.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using AngleSharp.Html.Parser; +using Jackett.Common.Models; +using Jackett.Common.Models.IndexerConfig; +using Jackett.Common.Services.Interfaces; +using Jackett.Common.Utils; +using Jackett.Common.Utils.Clients; +using Newtonsoft.Json.Linq; +using NLog; +using static Jackett.Common.Models.IndexerConfig.ConfigurationData; + +namespace Jackett.Common.Indexers +{ + [ExcludeFromCodeCoverage] + public class Libble : BaseWebIndexer + { + private string LandingUrl => SiteLink + "login.php"; + private string LoginUrl => SiteLink + "login.php"; + private string SearchUrl => SiteLink + "torrents.php"; + private Dictionary CategoryMappings = new Dictionary{ + { "cats_music", "Music" }, + { "cats_libblemixtapes", "Libble Mixtapes" }, + { "cats_musicvideos", "Music Videos" }, + }; + class VolumeFactorTag + { + public double DownloadVolumeFactor { get; set; } = 1.0; + public double UploadVolumeFactor { get; set; } = 1.0; + } + private Dictionary VolumeTagMappings = new Dictionary{ + { "Neutral!", new VolumeFactorTag + { + DownloadVolumeFactor = 0, + UploadVolumeFactor = 0 + } + }, + { "Freeleech!", new VolumeFactorTag + { + DownloadVolumeFactor = 0, + UploadVolumeFactor = 1 + } + }, + }; + + private new ConfigurationDataBasicLogin configData + { + get => (ConfigurationDataBasicLogin)base.configData; + set => base.configData = value; + } + + public Libble(IIndexerConfigurationService configService, WebClient wc, Logger l, IProtectionService ps) + : base(id: "libble", + name: "Libble", + description: "Libble is a Private Torrent Tracker for MUSIC", + link: "https://libble.me/", + caps: new TorznabCapabilities + { + TVSearchAvailable = false, + SupportedMusicSearchParamsList = new List { "q", "album", "artist", "label", "year" } + }, + configService: configService, + client: wc, + logger: l, + p: ps, + configData: new ConfigurationDataBasicLogin()) + { + Encoding = Encoding.UTF8; + Language = "en-us"; + Type = "private"; + + AddCategoryMapping(1, TorznabCatType.Audio, "Music"); + AddCategoryMapping(2, TorznabCatType.Audio, "Libble Mixtapes"); + AddCategoryMapping(7, TorznabCatType.AudioVideo, "Music Videos"); + } + + public override async Task GetConfigurationForSetup() + { + await RequestStringWithCookies(LandingUrl); + return configData; + } + + public override async Task ApplyConfiguration(JToken configJson) + { + LoadValuesFromJson(configJson); + + var pairs = new Dictionary + { + {"username", configData.Username.Value}, + {"password", configData.Password.Value}, + {"login", "Login"} + }; + + var result = await RequestLoginAndFollowRedirect(LoginUrl, pairs, null, true, SearchUrl, LandingUrl, true); + await ConfigureIfOK(result.Cookies, result.Content?.Contains("logout.php") == true, + () => + { + var parser = new HtmlParser(); + var dom = parser.ParseDocument(result.Content); + var warningNode = dom.QuerySelector("#loginform > .warning"); + var errorMessage = warningNode?.TextContent.Trim().Replace("\n\t", " "); + throw new ExceptionWithConfigData(errorMessage, configData); + }); + + + SaveConfig(); + + return IndexerConfigurationStatus.RequiresTesting; + } + + protected override async Task> PerformQuery(TorznabQuery query) + { + // https://libble.me/torrents.php?searchstr=the+used&taglist=&tags_type=1&order_by=time&order_way=desc + var releases = new List(); + var searchString = query.GetQueryString(); + var searchUrl = SearchUrl; + + var searchParams = new Dictionary { }; + var queryCollection = new NameValueCollection { }; + + // Search String + if (!string.IsNullOrWhiteSpace(query.ImdbID)) + queryCollection.Add("cataloguenumber", query.ImdbID); + else if (!string.IsNullOrWhiteSpace(searchString)) + queryCollection.Add("searchstr", searchString); + + + // Filter Categories + if (query.HasSpecifiedCategories) + { + foreach (var cat in MapTorznabCapsToTrackers(query)) + { + queryCollection.Add("filter_cat[" + cat.ToString() + "]", "1"); + } + } + + if (query.Artist != null) + queryCollection.Add("artistname", query.Artist); + + if (query.Label != null) + queryCollection.Add("recordlabel", query.Label); + + if (query.Year != null) + queryCollection.Add("year", query.Year.ToString()); + + if (query.Album != null) + queryCollection.Add("groupname", query.Album); + + searchUrl += "?" + queryCollection.GetQueryString(); + + var searchPage = await PostDataWithCookiesAndRetry(searchUrl, searchParams); + // Occasionally the cookies become invalid, login again if that happens + if (searchPage.IsRedirect) + { + await ApplyConfiguration(null); + searchPage = await PostDataWithCookiesAndRetry(searchUrl, searchParams); + } + + try + { + var parser = new HtmlParser(); + var dom = parser.ParseDocument(searchPage.Content); + var albumRows = dom.QuerySelectorAll("table#torrent_table > tbody > tr:has(strong > a[href*=\"torrents.php?id=\"])"); + foreach (var row in albumRows) + { + Regex releaseGroupRegex = new Regex(@"torrents\.php\?id=([0-9]+)"); + + var albumNameNode = row.QuerySelector("strong > a[href*=\"torrents.php?id=\"]"); + var artistsNameNodes = row.QuerySelectorAll("strong > a[href*=\"artist.php?id=\"]"); + var albumYearNode = albumNameNode.NextSibling; + var categoryNode = row.QuerySelector(".cats_col > div"); + var thumbnailNode = row.QuerySelector(".thumbnail"); + + var releaseArtist = "Various Artists"; + if (artistsNameNodes.Count() > 0) + { + List aristNames = new List(); + foreach (var aristNode in artistsNameNodes) + { + aristNames.Add(aristNode.TextContent.Trim()); + } + releaseArtist = string.Join(", ", aristNames); + } + + var releaseAlbumName = albumNameNode.TextContent.Trim(); + var releaseGroupId = ParseUtil.CoerceInt(releaseGroupRegex.Match(albumNameNode.GetAttribute("href")).Groups[1].ToString()); + var releaseAlbumYear = ParseUtil.CoerceInt(albumYearNode.TextContent.Replace("[", "").Replace("]", "").Trim()); + + Uri releaseThumbnailUri = null; + if (thumbnailNode != null) + releaseThumbnailUri = new Uri(thumbnailNode.GetAttribute("title")); + + ICollection releaseNewznabCategory = null; + var categoriesSplit = categoryNode.ClassName.Split(' '); + foreach (var rawCategory in categoriesSplit) + { + if (CategoryMappings.ContainsKey(rawCategory)) + { + var newznabCat = MapTrackerCatDescToNewznab(CategoryMappings[rawCategory]); + if (newznabCat.Count != 0) + releaseNewznabCategory = newznabCat; + } + } + + var releaseRows = dom.QuerySelectorAll(String.Format(".group_torrent.groupid_{0}", releaseGroupId)); + + string lastEdition = null; + foreach (var releaseDetails in releaseRows) + { + var editionInfoDetails = releaseDetails.QuerySelector(".edition_info"); + + // Process as release details + if (editionInfoDetails != null) + { + lastEdition = editionInfoDetails.QuerySelector("strong").TextContent; + } + // Process as torrent + else + { + // https://libble.me/torrents.php?id=51694&torrentid=89758 + var release = new ReleaseInfo(); + + var releaseMediaDetails = releaseDetails.Children[0].Children[1]; + var releaseFileCountDetails = releaseDetails.Children[1]; + var releaseDateDetails = releaseDetails.Children[2].Children[0]; + var releaseSizeDetails = releaseDetails.Children[3]; + var releaseGrabsDetails = releaseDetails.Children[4]; + var releaseSeedsCountDetails = releaseDetails.Children[5]; + var releasePeersCountDetails = releaseDetails.Children[6]; + var releaseDownloadDetails = releaseDetails.QuerySelector("a[href*=\"action=download\"]"); + var releaseMediaType = releaseMediaDetails.TextContent; + + release.Link = new Uri(SiteLink + releaseDownloadDetails.GetAttribute("href")); + release.Guid = release.Link; + release.Comments = new Uri(SiteLink + albumNameNode.GetAttribute("href")); + + // Aug 31 2020, 15:50 + try + { + release.PublishDate = DateTime.ParseExact( + releaseDateDetails.GetAttribute("title").Trim(), + "MMM dd yyyy, HH:mm", + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal + ); + } + catch (Exception) + { + } + + release.Files = ParseUtil.CoerceInt(releaseFileCountDetails.TextContent.Trim()); + release.Grabs = ParseUtil.CoerceInt(releaseGrabsDetails.TextContent.Trim()); + release.Seeders = ParseUtil.CoerceInt(releaseSeedsCountDetails.TextContent.Trim()); + release.Peers = release.Seeders + ParseUtil.CoerceInt(releasePeersCountDetails.TextContent.Trim()); + release.Size = ReleaseInfo.GetBytes(releaseSizeDetails.TextContent.Trim()); + release.BannerUrl = releaseThumbnailUri; + release.Category = releaseNewznabCategory; + release.MinimumSeedTime = 259200; // 72 hours + + // Attempt to find volume factor tag + release.DownloadVolumeFactor = 1; + release.UploadVolumeFactor = 1; + var releaseTags = releaseMediaType.Split('/').Select(tag => tag.Trim()).ToList(); + for (int i = releaseTags.Count - 1; i >= 0; i--) + { + string releaseTag = releaseTags[i]; + if (VolumeTagMappings.ContainsKey(releaseTag)) { + VolumeFactorTag volumeFactor = VolumeTagMappings[releaseTag]; + release.DownloadVolumeFactor = volumeFactor.DownloadVolumeFactor; + release.UploadVolumeFactor = volumeFactor.UploadVolumeFactor; + releaseTags.RemoveAt(i); + } + } + + // Set title (with volume factor tags stripped) + var releaseTagsString = string.Join(" / ", releaseTags); + release.Title = String.Format("{0} - {1} [{2}] {3}", releaseArtist, releaseAlbumName, releaseAlbumYear, releaseTagsString); + + releases.Add(release); + } + } + } + } + catch (Exception ex) + { + OnParseError(searchPage.Content, ex); + } + + return releases; + } + } +}