Skip to content

Commit 9bcee6a

Browse files
committed
feat: search by reddit post
1 parent 4c1ace2 commit 9bcee6a

15 files changed

Lines changed: 393 additions & 31 deletions

File tree

ApplicationData/Services/LinkProvider.cs

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -81,35 +81,36 @@ FROM links l
8181
);
8282
}
8383

84-
public async Task<IReadOnlyCollection<Link>> FindAllLinksByPostId(string redditPostId)
84+
public async IAsyncEnumerable<Link> FindAllLinksByPostId(string redditPostId, [EnumeratorCancellation] CancellationToken cancellationToken)
8585
{
8686
await using var connection = _dbConnectionFactory.CreateReadOnlyConnection();
87-
var linkQueryResult = await connection.Query<LinkQueryResult>(
87+
var linkQueryResult = connection.QueryUnbuffered<LinkQueryResult>(
8888
"""
89-
SELECT
90-
l.link_id AS LinkId
91-
,l.reddit_post_id AS RedditPostId
92-
,l.link_url AS LinkUrl
93-
,l.link_type AS LinkType
94-
,l.created_at AS CreatedAt
95-
,l.owner AS OwnerId
96-
FROM links l
97-
WHERE
98-
l.reddit_post_id = @redditPostId
99-
AND l.is_deleted = false
100-
""",
101-
new { redditPostId });
89+
SELECT
90+
l.link_id AS LinkId
91+
,l.reddit_post_id AS RedditPostId
92+
,l.link_url AS LinkUrl
93+
,l.link_type AS LinkType
94+
,l.created_at AS CreatedAt
95+
,l.owner AS OwnerId
96+
FROM links l
97+
WHERE
98+
l.reddit_post_id = @redditPostId
99+
AND l.is_deleted = false
100+
""",
101+
new { redditPostId }, cancellationToken);
102102

103-
return linkQueryResult
104-
.Select(result => new Link(
105-
linkId: result.LinkId,
106-
redditPostId: result.RedditPostId,
107-
linkUrl: result.LinkUrl,
108-
linkType: ParseRawLinkType(result.LinkType),
109-
createdAt: result.CreatedAt,
110-
ownerId: result.OwnerId
111-
))
112-
.ToArray();
103+
await foreach (var result in linkQueryResult)
104+
{
105+
yield return new Link(
106+
linkId: result.LinkId,
107+
redditPostId: result.RedditPostId,
108+
linkUrl: result.LinkUrl,
109+
linkType: ParseRawLinkType(result.LinkType),
110+
createdAt: result.CreatedAt,
111+
ownerId: result.OwnerId
112+
);
113+
}
113114
}
114115

115116
public async Task DeleteLinkById(int linkId)

WebApi/Controllers/PublicController.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,20 @@ UserProvider userProvider
3535
/// Find links available for the specified reddit post ID.
3636
/// </summary>
3737
/// <param name="redditPostId">The unique reddit post ID. For example, use "sf8kp8" from the permalink "https://www.reddit.com/r/aww/comments/sf8kp8/treats_you_say/".</param>
38+
/// <param name="cancellationToken"></param>
3839
[HttpGet]
3940
[Route("Link/By-Reddit-Post-Id/{redditPostId}")]
4041
[SwaggerResponse(StatusCodes.Status200OK,
4142
description: "OK. A list of links available for this reddit post ID.",
4243
type: typeof(IReadOnlyCollection<PublicLinkListingResult>))]
43-
public async Task<IReadOnlyCollection<PublicLinkListingResult>> FindLinksByPostId(string redditPostId)
44+
public async Task<IReadOnlyCollection<PublicLinkListingResult>> FindLinksByPostId(string redditPostId, CancellationToken cancellationToken)
4445
{
45-
var linksByPostId = await _linkProvider.FindAllLinksByPostId(redditPostId);
46-
return await linksByPostId.ToAsyncEnumerable()
46+
return await _linkProvider.FindAllLinksByPostId(redditPostId, cancellationToken)
4747
.SelectAwait(async link => new PublicLinkListingResult(
4848
link.LinkUrl,
4949
LinkTypeHelpers.ParseToSerializableLinkType(link.LinkType.RawValue),
5050
ProviderUsername: (await _userProvider.FindUserByIdIncludeDeleted(link.OwnerId)).Map(x => x.DisplayUsername).Value))
51-
.ToArrayAsync();
51+
.ToArrayAsync(cancellationToken: cancellationToken);
5252
}
5353

5454
/// <summary>

WebApi/Pages/Error.cshtml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@page
2+
@model ErrorModel
3+
@{
4+
ViewData["Title"] = "Error";
5+
}
6+
7+
<h1 class="text-danger">Error.</h1>
8+
<h2 class="text-danger">An error occurred while processing your request.</h2>
9+
10+
@if (Model.ShowRequestId)
11+
{
12+
<p>
13+
<strong>Request ID:</strong> <code>@Model.RequestId</code>
14+
</p>
15+
}
16+
17+
<h3>Development Mode</h3>
18+
<p>
19+
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
20+
</p>
21+
<p>
22+
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
23+
It can result in displaying sensitive information from exceptions to end users.
24+
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
25+
and restarting the app.
26+
</p>

WebApi/Pages/Error.cshtml.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Diagnostics;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.AspNetCore.Mvc.RazorPages;
4+
5+
namespace WebApi.Pages;
6+
7+
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
8+
[IgnoreAntiforgeryToken]
9+
public class ErrorModel : PageModel
10+
{
11+
public string? RequestId { get; set; }
12+
13+
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
14+
15+
private readonly ILogger<ErrorModel> _logger;
16+
17+
public ErrorModel(ILogger<ErrorModel> logger)
18+
{
19+
_logger = logger;
20+
}
21+
22+
public void OnGet()
23+
{
24+
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
25+
}
26+
}

WebApi/Pages/Index.cshtml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
@page
2+
@attribute [AllowAnonymous]
3+
@using Microsoft.AspNetCore.Authorization
4+
@model IndexModel
5+
@{
6+
ViewData["Title"] = "search database";
7+
}
8+
9+
<div class="text-center" xmlns="http://www.w3.org/1999/html">
10+
<h1 class="display-4">a-mirror database</h1>
11+
<p>search for a video mirror using the id of a reddit post.</p>
12+
<p>example: <a href="https://www.reddit.com/r/PublicFreakout/comments/j2vitm/she_just_got_hired/">https://www.reddit.com/r/PublicFreakout/comments/<mark>j2vitm</mark>/she_just_got_hired/</a></p>
13+
14+
<form method="get">
15+
<label for="redditPostId">reddit post id</label>
16+
<input type="text" name="redditPostId" id="redditPostId" placeholder="e.g., j2vitm" value="@Model.RedditPostId"/>
17+
<span class="text-danger">
18+
@Html.ValidationMessageFor(m => m.RedditPostId)
19+
</span>
20+
21+
<button type="submit" class="btn btn-primary">search</button>
22+
</form>
23+
24+
@if (Model.SearchResults.Try(out var links))
25+
{
26+
<h2>results</h2>
27+
<p><a href="https://reddit.com/@Model.RedditPostId" target="_blank">(open reddit post)</a></p>
28+
29+
@if (links.Count == 0)
30+
{
31+
<p>no links found. sorry about that.</p>
32+
}
33+
else
34+
{
35+
var mirrors = links.Where(x => x.Link.LinkType.IsMirror).ToArray();
36+
var downloads = links.Where(x => x.Link.LinkType.IsDownload).ToArray();
37+
38+
<p>
39+
found <strong>@mirrors.Length</strong> @(mirrors.Length == 1 ? "mirror" : "mirrors")
40+
and <strong>@downloads.Length</strong> @(downloads.Length == 1 ? "download" : "downloads").
41+
</p>
42+
43+
<div class="results-container">
44+
<div class="linktype-container">
45+
<h3>mirrors</h3>
46+
47+
@if (mirrors.Length == 0)
48+
{
49+
<p>(none)</p>
50+
}
51+
else
52+
{
53+
<ul>
54+
@foreach (var (mirror, i) in mirrors.Select((x, i) => (x, i)))
55+
{
56+
<li>
57+
<a href="@mirror.Link.LinkUrl" target="_blank">mirror #@(i + 1)</a>
58+
(provided by <a href="https://reddit.com/user/@mirror.Owner.DisplayUsername" target="_blank">/u/@mirror.Owner.DisplayUsername</a>)
59+
</li>
60+
}
61+
</ul>
62+
}
63+
</div>
64+
65+
<div class="linktype-container">
66+
<h3>downloads</h3>
67+
68+
@if (downloads.Length == 0)
69+
{
70+
<p>(none)</p>
71+
}
72+
else
73+
{
74+
<ul>
75+
@foreach (var (download, i) in downloads.Select((x, i) => (x, i)))
76+
{
77+
<li>
78+
<a href="@download.Link.LinkUrl" target="_blank">download #@(i + 1)</a>
79+
(provided by <a href="https://reddit.com/user/@download.Owner.DisplayUsername" target="_blank">/u/@download.Owner.DisplayUsername</a>)
80+
</li>
81+
}
82+
</ul>
83+
}
84+
</div>
85+
</div>
86+
}
87+
}
88+
</div>

WebApi/Pages/Index.cshtml.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using ApplicationData.Services;
2+
using DataClasses;
3+
using FruityFoundation.Base.Structures;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.Mvc.RazorPages;
6+
7+
namespace WebApi.Pages;
8+
9+
/// <summary>
10+
/// Index Model
11+
/// </summary>
12+
public class IndexModel : PageModel
13+
{
14+
private readonly LinkProvider _linkProvider;
15+
private readonly UserProvider _userProvider;
16+
17+
/// <summary>
18+
/// C'tor
19+
/// </summary>
20+
public IndexModel(LinkProvider linkProvider, UserProvider userProvider)
21+
{
22+
_linkProvider = linkProvider;
23+
_userProvider = userProvider;
24+
}
25+
26+
/// <summary>
27+
/// Search results
28+
/// </summary>
29+
public Maybe<IReadOnlyCollection<(Link Link, User Owner)>> SearchResults = Maybe.Empty<IReadOnlyCollection<(Link, User)>>();
30+
31+
/// <summary>
32+
/// Reddit Post Id
33+
/// </summary>
34+
[BindProperty(SupportsGet = true)]
35+
public string? RedditPostId { get; set; }
36+
37+
/// <summary>
38+
/// GET /
39+
/// </summary>
40+
public async Task OnGet(CancellationToken cancellationToken)
41+
{
42+
if (string.IsNullOrEmpty(RedditPostId))
43+
return;
44+
45+
var linksWithOwners = await _linkProvider.FindAllLinksByPostId(RedditPostId, cancellationToken)
46+
.SelectAwait(async link =>
47+
{
48+
if (!(await _userProvider.FindUserByIdIncludeDeleted(link.OwnerId)).Try(out var owner))
49+
throw new ApplicationException($"Link {link.LinkId} has an owner ID {link.OwnerId} that does not exist.");
50+
51+
return (Link: link, Owner: owner);
52+
})
53+
.ToArrayAsync(cancellationToken);
54+
55+
SearchResults = Maybe.Create<IReadOnlyCollection<(Link Link, User Owner)>>(linksWithOwners);
56+
}
57+
}

WebApi/Pages/Index.cshtml.css

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
mark {
2+
font-weight: bold;
3+
}
4+
5+
.results-container {
6+
display: flex;
7+
flex-direction: row;
8+
justify-content: center;
9+
margin-left: auto;
10+
margin-right: auto;
11+
max-width: 1000px;
12+
}
13+
14+
.linktype-container {
15+
margin: 2em;
16+
max-width: 500px;
17+
}
18+
19+
ul li {
20+
text-align: left;
21+
}

WebApi/Pages/Shared/_Layout.cshtml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8"/>
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6+
<title>@ViewData["Title"] - a-mirror</title>
7+
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
8+
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
9+
<link rel="stylesheet" href="~/WebApi.styles.css" asp-append-version="true"/>
10+
</head>
11+
<body>
12+
<div class="container">
13+
<main role="main" class="pb-3">
14+
@RenderBody()
15+
</main>
16+
</div>
17+
18+
<footer class="border-top footer text-muted">
19+
<div class="container">
20+
a-centalized-mirror
21+
<ul class="list-inline d-inline ms-2">
22+
<li class="list-inline-item"><a href="https://amirror.link/source">source code</a></li>
23+
<li class="list-inline-item"><a href="/docs">api docs</a></li>
24+
</ul>
25+
</div>
26+
</footer>
27+
28+
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
29+
30+
@await RenderSectionAsync("Scripts", required: false)
31+
</body>
32+
</html>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
2+
for details on configuring this project to bundle and minify static web assets. */
3+
4+
a.navbar-brand {
5+
white-space: normal;
6+
text-align: center;
7+
word-break: break-all;
8+
}
9+
10+
a {
11+
color: #0077cc;
12+
}
13+
14+
.btn-primary {
15+
color: #fff;
16+
background-color: #1b6ec2;
17+
border-color: #1861ac;
18+
}
19+
20+
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
21+
color: #fff;
22+
background-color: #1b6ec2;
23+
border-color: #1861ac;
24+
}
25+
26+
.border-top {
27+
border-top: 1px solid #e5e5e5;
28+
}
29+
.border-bottom {
30+
border-bottom: 1px solid #e5e5e5;
31+
}
32+
33+
.box-shadow {
34+
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
35+
}
36+
37+
button.accept-policy {
38+
font-size: 1rem;
39+
line-height: inherit;
40+
}
41+
42+
.footer {
43+
position: absolute;
44+
bottom: 0;
45+
width: 100%;
46+
white-space: nowrap;
47+
line-height: 60px;
48+
}

WebApi/Pages/_ViewImports.cshtml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@namespace WebApi.Pages
2+
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

0 commit comments

Comments
 (0)