diff --git a/frontend/src/Search/SearchFooter.js b/frontend/src/Search/SearchFooter.js index cd3897f33..36dd4acaa 100644 --- a/frontend/src/Search/SearchFooter.js +++ b/frontend/src/Search/SearchFooter.js @@ -37,7 +37,10 @@ class SearchFooter extends Component { searchingReleases: false, searchQuery: defaultSearchQuery || '', searchIndexerIds: defaultIndexerIds, - searchCategories: defaultCategories + searchCategories: defaultCategories, + searchLimit: 100, + searchOffset: 0, + newSearch: true }; } @@ -115,11 +118,28 @@ class SearchFooter extends Component { }; onSearchPress = () => { - this.props.onSearchPress(this.state.searchQuery, this.state.searchIndexerIds, this.state.searchCategories, this.state.searchType); + + const { + searchLimit, + searchOffset, + searchQuery, + searchIndexerIds, + searchCategories, + searchType + } = this.state; + + this.props.onSearchPress(searchQuery, searchIndexerIds, searchCategories, searchType, searchLimit, searchOffset); + + this.setState({ searchOffset: searchOffset + 100, newSearch: false }); }; onSearchInputChange = ({ value }) => { - this.setState({ searchQuery: value }); + this.setState({ searchQuery: value, newSearch: true, searchOffset: 0 }); + }; + + onInputChange = ({ name, value }) => { + this.props.onInputChange({ name, value }); + this.setState({ newSearch: true, searchOffset: 0 }); }; // @@ -141,6 +161,7 @@ class SearchFooter extends Component { searchQuery, searchIndexerIds, searchCategories, + newSearch, isQueryParameterModalOpen, queryModalOptions, searchType @@ -206,7 +227,7 @@ class SearchFooter extends Component { name='searchIndexerIds' value={searchIndexerIds} isDisabled={isFetching} - onChange={onInputChange} + onChange={this.onInputChange} /> @@ -220,7 +241,7 @@ class SearchFooter extends Component { name='searchCategories' value={searchCategories} isDisabled={isFetching} - onChange={onInputChange} + onChange={this.onInputChange} /> @@ -253,7 +274,7 @@ class SearchFooter extends Component { isDisabled={isFetching || !hasIndexers} onPress={this.onSearchPress} > - {translate('Search')} + {newSearch ? translate('Search') : translate('More')} diff --git a/frontend/src/Search/SearchIndex.js b/frontend/src/Search/SearchIndex.js index 06479972b..5a7d9d484 100644 --- a/frontend/src/Search/SearchIndex.js +++ b/frontend/src/Search/SearchIndex.js @@ -196,8 +196,8 @@ class SearchIndex extends Component { this.setState({ jumpToCharacter }); }; - onSearchPress = (query, indexerIds, categories, type) => { - this.props.onSearchPress({ query, indexerIds, categories, type }); + onSearchPress = (query, indexerIds, categories, type, limit, offset) => { + this.props.onSearchPress({ query, indexerIds, categories, type, limit, offset }); }; onBulkGrabPress = () => { diff --git a/src/Prowlarr.Api.V1/Search/ReleaseResource.cs b/src/Prowlarr.Api.V1/Search/ReleaseResource.cs new file mode 100644 index 000000000..6654d82b0 --- /dev/null +++ b/src/Prowlarr.Api.V1/Search/ReleaseResource.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser.Model; +using Prowlarr.Http.REST; + +namespace Prowlarr.Api.V1.Search +{ + public class ReleaseResource : RestResource + { + public string Guid { get; set; } + public int Age { get; set; } + public double AgeHours { get; set; } + public double AgeMinutes { get; set; } + public long Size { get; set; } + public int? Files { get; set; } + public int? Grabs { get; set; } + public int IndexerId { get; set; } + public string Indexer { get; set; } + public string SubGroup { get; set; } + public string ReleaseHash { get; set; } + public string Title { get; set; } + public bool Approved { get; set; } + public int ImdbId { get; set; } + public DateTime PublishDate { get; set; } + public string CommentUrl { get; set; } + public string DownloadUrl { get; set; } + public string InfoUrl { get; set; } + public string PosterUrl { get; set; } + public IEnumerable IndexerFlags { get; set; } + public ICollection Categories { get; set; } + + public string MagnetUrl { get; set; } + public string InfoHash { get; set; } + public int? Seeders { get; set; } + public int? Leechers { get; set; } + public DownloadProtocol Protocol { get; set; } + } + + public static class ReleaseResourceMapper + { + public static ReleaseResource ToResource(this ReleaseInfo model) + { + var releaseInfo = model; + var torrentInfo = (model as TorrentInfo) ?? new TorrentInfo(); + var indexerFlags = torrentInfo.IndexerFlags.Select(f => f.Name); + + // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?) + return new ReleaseResource + { + Guid = releaseInfo.Guid, + + //QualityWeight + Age = releaseInfo.Age, + AgeHours = releaseInfo.AgeHours, + AgeMinutes = releaseInfo.AgeMinutes, + Size = releaseInfo.Size ?? 0, + Files = releaseInfo.Files, + Grabs = releaseInfo.Grabs, + IndexerId = releaseInfo.IndexerId, + Indexer = releaseInfo.Indexer, + Title = releaseInfo.Title, + ImdbId = releaseInfo.ImdbId, + PublishDate = releaseInfo.PublishDate, + CommentUrl = releaseInfo.CommentUrl, + DownloadUrl = releaseInfo.DownloadUrl, + InfoUrl = releaseInfo.InfoUrl, + PosterUrl = releaseInfo.PosterUrl, + Categories = releaseInfo.Categories, + + //ReleaseWeight + MagnetUrl = torrentInfo.MagnetUrl, + InfoHash = torrentInfo.InfoHash, + Seeders = torrentInfo.Seeders, + Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, + Protocol = releaseInfo.DownloadProtocol, + IndexerFlags = indexerFlags + }; + } + + public static ReleaseInfo ToModel(this ReleaseResource resource) + { + ReleaseInfo model; + + if (resource.Protocol == DownloadProtocol.Torrent) + { + model = new TorrentInfo + { + MagnetUrl = resource.MagnetUrl, + InfoHash = resource.InfoHash, + Seeders = resource.Seeders, + Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null + }; + } + else + { + model = new ReleaseInfo(); + } + + model.Guid = resource.Guid; + model.Title = resource.Title; + model.Size = resource.Size; + model.DownloadUrl = resource.DownloadUrl; + model.InfoUrl = resource.InfoUrl; + model.PosterUrl = resource.PosterUrl; + model.CommentUrl = resource.CommentUrl; + model.IndexerId = resource.IndexerId; + model.Indexer = resource.Indexer; + model.DownloadProtocol = resource.Protocol; + model.ImdbId = resource.ImdbId; + model.PublishDate = resource.PublishDate.ToUniversalTime(); + + return model; + } + } +} diff --git a/src/Prowlarr.Api.V1/Search/SearchController.cs b/src/Prowlarr.Api.V1/Search/SearchController.cs index 836bfdaca..91e8c02f6 100644 --- a/src/Prowlarr.Api.V1/Search/SearchController.cs +++ b/src/Prowlarr.Api.V1/Search/SearchController.cs @@ -22,7 +22,7 @@ using Prowlarr.Http.REST; namespace Prowlarr.Api.V1.Search { [V1ApiController] - public class SearchController : RestController + public class SearchController : RestController { private readonly ISearchForNzb _nzbSearhService; private readonly IDownloadService _downloadService; @@ -46,13 +46,13 @@ namespace Prowlarr.Api.V1.Search _remoteReleaseCache = cacheManager.GetCache(GetType(), "remoteReleases"); } - public override SearchResource GetResourceById(int id) + public override ReleaseResource GetResourceById(int id) { throw new NotImplementedException(); } [HttpPost] - public ActionResult GrabRelease(SearchResource release) + public ActionResult GrabRelease(ReleaseResource release) { ValidateResource(release); @@ -76,7 +76,7 @@ namespace Prowlarr.Api.V1.Search } [HttpPost("bulk")] - public ActionResult GrabReleases(List releases) + public ActionResult GrabReleases(List releases) { var source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]); var host = Request.GetHostName(); @@ -108,35 +108,30 @@ namespace Prowlarr.Api.V1.Search } [HttpGet] - public Task> GetAll(string query, [FromQuery] List indexerIds, [FromQuery] List categories, [FromQuery] string type = "search") + public Task> GetAll([FromQuery] SearchResource payload) { - if (indexerIds.Any()) - { - return GetSearchReleases(query, type, indexerIds, categories); - } - else - { - return GetSearchReleases(query, type, null, categories); - } + return GetSearchReleases(payload); } - private async Task> GetSearchReleases(string query, string type, List indexerIds, List categories) + private async Task> GetSearchReleases([FromQuery] SearchResource payload) { try { var request = new NewznabRequest { - q = query, - t = type, + q = payload.Query, + t = payload.Type, source = "Prowlarr", - cat = string.Join(",", categories), + cat = string.Join(",", payload.Categories), server = Request.GetServerUrl(), - host = Request.GetHostName() + host = Request.GetHostName(), + limit = payload.Limit, + offset = payload.Offset }; request.QueryToParams(); - var result = await _nzbSearhService.Search(request, indexerIds, true); + var result = await _nzbSearhService.Search(request, payload.IndexerIds, true); var decisions = result.Releases; return MapDecisions(decisions, Request.GetServerUrl()); @@ -150,12 +145,12 @@ namespace Prowlarr.Api.V1.Search _logger.Error(ex, "Search failed: " + ex.Message); } - return new List(); + return new List(); } - protected virtual List MapDecisions(IEnumerable releases, string serverUrl) + protected virtual List MapDecisions(IEnumerable releases, string serverUrl) { - var result = new List(); + var result = new List(); foreach (var downloadDecision in releases) { @@ -173,7 +168,7 @@ namespace Prowlarr.Api.V1.Search return result; } - private string GetCacheKey(SearchResource resource) + private string GetCacheKey(ReleaseResource resource) { return string.Concat(resource.IndexerId, "_", resource.Guid); } diff --git a/src/Prowlarr.Api.V1/Search/SearchResource.cs b/src/Prowlarr.Api.V1/Search/SearchResource.cs index 15ddf9f08..4bae4eabb 100644 --- a/src/Prowlarr.Api.V1/Search/SearchResource.cs +++ b/src/Prowlarr.Api.V1/Search/SearchResource.cs @@ -1,117 +1,24 @@ using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Parser.Model; -using Prowlarr.Http.REST; +using System.Text; +using System.Threading.Tasks; namespace Prowlarr.Api.V1.Search { - public class SearchResource : RestResource + public class SearchResource { - public string Guid { get; set; } - public int Age { get; set; } - public double AgeHours { get; set; } - public double AgeMinutes { get; set; } - public long Size { get; set; } - public int? Files { get; set; } - public int? Grabs { get; set; } - public int IndexerId { get; set; } - public string Indexer { get; set; } - public string SubGroup { get; set; } - public string ReleaseHash { get; set; } - public string Title { get; set; } - public bool Approved { get; set; } - public int ImdbId { get; set; } - public DateTime PublishDate { get; set; } - public string CommentUrl { get; set; } - public string DownloadUrl { get; set; } - public string InfoUrl { get; set; } - public string PosterUrl { get; set; } - public IEnumerable IndexerFlags { get; set; } - public ICollection Categories { get; set; } - - public string MagnetUrl { get; set; } - public string InfoHash { get; set; } - public int? Seeders { get; set; } - public int? Leechers { get; set; } - public DownloadProtocol Protocol { get; set; } - } - - public static class ReleaseResourceMapper - { - public static SearchResource ToResource(this ReleaseInfo model) + public SearchResource() { - var releaseInfo = model; - var torrentInfo = (model as TorrentInfo) ?? new TorrentInfo(); - var indexerFlags = torrentInfo.IndexerFlags.Select(f => f.Name); - - // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?) - return new SearchResource - { - Guid = releaseInfo.Guid, - - //QualityWeight - Age = releaseInfo.Age, - AgeHours = releaseInfo.AgeHours, - AgeMinutes = releaseInfo.AgeMinutes, - Size = releaseInfo.Size ?? 0, - Files = releaseInfo.Files, - Grabs = releaseInfo.Grabs, - IndexerId = releaseInfo.IndexerId, - Indexer = releaseInfo.Indexer, - Title = releaseInfo.Title, - ImdbId = releaseInfo.ImdbId, - PublishDate = releaseInfo.PublishDate, - CommentUrl = releaseInfo.CommentUrl, - DownloadUrl = releaseInfo.DownloadUrl, - InfoUrl = releaseInfo.InfoUrl, - PosterUrl = releaseInfo.PosterUrl, - Categories = releaseInfo.Categories, - - //ReleaseWeight - MagnetUrl = torrentInfo.MagnetUrl, - InfoHash = torrentInfo.InfoHash, - Seeders = torrentInfo.Seeders, - Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, - Protocol = releaseInfo.DownloadProtocol, - IndexerFlags = indexerFlags - }; + Type = "search"; + Categories = new List(); } - public static ReleaseInfo ToModel(this SearchResource resource) - { - ReleaseInfo model; - - if (resource.Protocol == DownloadProtocol.Torrent) - { - model = new TorrentInfo - { - MagnetUrl = resource.MagnetUrl, - InfoHash = resource.InfoHash, - Seeders = resource.Seeders, - Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null - }; - } - else - { - model = new ReleaseInfo(); - } - - model.Guid = resource.Guid; - model.Title = resource.Title; - model.Size = resource.Size; - model.DownloadUrl = resource.DownloadUrl; - model.InfoUrl = resource.InfoUrl; - model.PosterUrl = resource.PosterUrl; - model.CommentUrl = resource.CommentUrl; - model.IndexerId = resource.IndexerId; - model.Indexer = resource.Indexer; - model.DownloadProtocol = resource.Protocol; - model.ImdbId = resource.ImdbId; - model.PublishDate = resource.PublishDate.ToUniversalTime(); - - return model; - } + public string Query { get; set; } + public string Type { get; set; } + public List IndexerIds { get; set; } + public List Categories { get; set; } + public int Limit { get; set; } + public int Offset { get; set; } } }