mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Load more (page) results on Search UI
This commit is contained in:
@@ -37,7 +37,10 @@ class SearchFooter extends Component {
|
|||||||
searchingReleases: false,
|
searchingReleases: false,
|
||||||
searchQuery: defaultSearchQuery || '',
|
searchQuery: defaultSearchQuery || '',
|
||||||
searchIndexerIds: defaultIndexerIds,
|
searchIndexerIds: defaultIndexerIds,
|
||||||
searchCategories: defaultCategories
|
searchCategories: defaultCategories,
|
||||||
|
searchLimit: 100,
|
||||||
|
searchOffset: 0,
|
||||||
|
newSearch: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +118,28 @@ class SearchFooter extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onSearchPress = () => {
|
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 }) => {
|
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,
|
searchQuery,
|
||||||
searchIndexerIds,
|
searchIndexerIds,
|
||||||
searchCategories,
|
searchCategories,
|
||||||
|
newSearch,
|
||||||
isQueryParameterModalOpen,
|
isQueryParameterModalOpen,
|
||||||
queryModalOptions,
|
queryModalOptions,
|
||||||
searchType
|
searchType
|
||||||
@@ -206,7 +227,7 @@ class SearchFooter extends Component {
|
|||||||
name='searchIndexerIds'
|
name='searchIndexerIds'
|
||||||
value={searchIndexerIds}
|
value={searchIndexerIds}
|
||||||
isDisabled={isFetching}
|
isDisabled={isFetching}
|
||||||
onChange={onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -220,7 +241,7 @@ class SearchFooter extends Component {
|
|||||||
name='searchCategories'
|
name='searchCategories'
|
||||||
value={searchCategories}
|
value={searchCategories}
|
||||||
isDisabled={isFetching}
|
isDisabled={isFetching}
|
||||||
onChange={onInputChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -253,7 +274,7 @@ class SearchFooter extends Component {
|
|||||||
isDisabled={isFetching || !hasIndexers}
|
isDisabled={isFetching || !hasIndexers}
|
||||||
onPress={this.onSearchPress}
|
onPress={this.onSearchPress}
|
||||||
>
|
>
|
||||||
{translate('Search')}
|
{newSearch ? translate('Search') : translate('More')}
|
||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -196,8 +196,8 @@ class SearchIndex extends Component {
|
|||||||
this.setState({ jumpToCharacter });
|
this.setState({ jumpToCharacter });
|
||||||
};
|
};
|
||||||
|
|
||||||
onSearchPress = (query, indexerIds, categories, type) => {
|
onSearchPress = (query, indexerIds, categories, type, limit, offset) => {
|
||||||
this.props.onSearchPress({ query, indexerIds, categories, type });
|
this.props.onSearchPress({ query, indexerIds, categories, type, limit, offset });
|
||||||
};
|
};
|
||||||
|
|
||||||
onBulkGrabPress = () => {
|
onBulkGrabPress = () => {
|
||||||
|
117
src/Prowlarr.Api.V1/Search/ReleaseResource.cs
Normal file
117
src/Prowlarr.Api.V1/Search/ReleaseResource.cs
Normal file
@@ -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<string> IndexerFlags { get; set; }
|
||||||
|
public ICollection<IndexerCategory> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -22,7 +22,7 @@ using Prowlarr.Http.REST;
|
|||||||
namespace Prowlarr.Api.V1.Search
|
namespace Prowlarr.Api.V1.Search
|
||||||
{
|
{
|
||||||
[V1ApiController]
|
[V1ApiController]
|
||||||
public class SearchController : RestController<SearchResource>
|
public class SearchController : RestController<ReleaseResource>
|
||||||
{
|
{
|
||||||
private readonly ISearchForNzb _nzbSearhService;
|
private readonly ISearchForNzb _nzbSearhService;
|
||||||
private readonly IDownloadService _downloadService;
|
private readonly IDownloadService _downloadService;
|
||||||
@@ -46,13 +46,13 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
_remoteReleaseCache = cacheManager.GetCache<ReleaseInfo>(GetType(), "remoteReleases");
|
_remoteReleaseCache = cacheManager.GetCache<ReleaseInfo>(GetType(), "remoteReleases");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SearchResource GetResourceById(int id)
|
public override ReleaseResource GetResourceById(int id)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public ActionResult<SearchResource> GrabRelease(SearchResource release)
|
public ActionResult<ReleaseResource> GrabRelease(ReleaseResource release)
|
||||||
{
|
{
|
||||||
ValidateResource(release);
|
ValidateResource(release);
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("bulk")]
|
[HttpPost("bulk")]
|
||||||
public ActionResult<SearchResource> GrabReleases(List<SearchResource> releases)
|
public ActionResult<ReleaseResource> GrabReleases(List<ReleaseResource> releases)
|
||||||
{
|
{
|
||||||
var source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]);
|
var source = UserAgentParser.ParseSource(Request.Headers["User-Agent"]);
|
||||||
var host = Request.GetHostName();
|
var host = Request.GetHostName();
|
||||||
@@ -108,35 +108,30 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public Task<List<SearchResource>> GetAll(string query, [FromQuery] List<int> indexerIds, [FromQuery] List<int> categories, [FromQuery] string type = "search")
|
public Task<List<ReleaseResource>> GetAll([FromQuery] SearchResource payload)
|
||||||
{
|
{
|
||||||
if (indexerIds.Any())
|
return GetSearchReleases(payload);
|
||||||
{
|
|
||||||
return GetSearchReleases(query, type, indexerIds, categories);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return GetSearchReleases(query, type, null, categories);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<SearchResource>> GetSearchReleases(string query, string type, List<int> indexerIds, List<int> categories)
|
private async Task<List<ReleaseResource>> GetSearchReleases([FromQuery] SearchResource payload)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var request = new NewznabRequest
|
var request = new NewznabRequest
|
||||||
{
|
{
|
||||||
q = query,
|
q = payload.Query,
|
||||||
t = type,
|
t = payload.Type,
|
||||||
source = "Prowlarr",
|
source = "Prowlarr",
|
||||||
cat = string.Join(",", categories),
|
cat = string.Join(",", payload.Categories),
|
||||||
server = Request.GetServerUrl(),
|
server = Request.GetServerUrl(),
|
||||||
host = Request.GetHostName()
|
host = Request.GetHostName(),
|
||||||
|
limit = payload.Limit,
|
||||||
|
offset = payload.Offset
|
||||||
};
|
};
|
||||||
|
|
||||||
request.QueryToParams();
|
request.QueryToParams();
|
||||||
|
|
||||||
var result = await _nzbSearhService.Search(request, indexerIds, true);
|
var result = await _nzbSearhService.Search(request, payload.IndexerIds, true);
|
||||||
var decisions = result.Releases;
|
var decisions = result.Releases;
|
||||||
|
|
||||||
return MapDecisions(decisions, Request.GetServerUrl());
|
return MapDecisions(decisions, Request.GetServerUrl());
|
||||||
@@ -150,12 +145,12 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
_logger.Error(ex, "Search failed: " + ex.Message);
|
_logger.Error(ex, "Search failed: " + ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new List<SearchResource>();
|
return new List<ReleaseResource>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual List<SearchResource> MapDecisions(IEnumerable<ReleaseInfo> releases, string serverUrl)
|
protected virtual List<ReleaseResource> MapDecisions(IEnumerable<ReleaseInfo> releases, string serverUrl)
|
||||||
{
|
{
|
||||||
var result = new List<SearchResource>();
|
var result = new List<ReleaseResource>();
|
||||||
|
|
||||||
foreach (var downloadDecision in releases)
|
foreach (var downloadDecision in releases)
|
||||||
{
|
{
|
||||||
@@ -173,7 +168,7 @@ namespace Prowlarr.Api.V1.Search
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetCacheKey(SearchResource resource)
|
private string GetCacheKey(ReleaseResource resource)
|
||||||
{
|
{
|
||||||
return string.Concat(resource.IndexerId, "_", resource.Guid);
|
return string.Concat(resource.IndexerId, "_", resource.Guid);
|
||||||
}
|
}
|
||||||
|
@@ -1,117 +1,24 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Core.Indexers;
|
using System.Text;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using System.Threading.Tasks;
|
||||||
using Prowlarr.Http.REST;
|
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Search
|
namespace Prowlarr.Api.V1.Search
|
||||||
{
|
{
|
||||||
public class SearchResource : RestResource
|
public class SearchResource
|
||||||
{
|
{
|
||||||
public string Guid { get; set; }
|
public SearchResource()
|
||||||
public int Age { get; set; }
|
{
|
||||||
public double AgeHours { get; set; }
|
Type = "search";
|
||||||
public double AgeMinutes { get; set; }
|
Categories = new List<int>();
|
||||||
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<string> IndexerFlags { get; set; }
|
|
||||||
public ICollection<IndexerCategory> 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 string Query { get; set; }
|
||||||
{
|
public string Type { get; set; }
|
||||||
public static SearchResource ToResource(this ReleaseInfo model)
|
public List<int> IndexerIds { get; set; }
|
||||||
{
|
public List<int> Categories { get; set; }
|
||||||
var releaseInfo = model;
|
public int Limit { get; set; }
|
||||||
var torrentInfo = (model as TorrentInfo) ?? new TorrentInfo();
|
public int Offset { get; set; }
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user