Skip to content

Commit ff16c83

Browse files
authored
Merge branch 'rolling' into dependabot/github_actions/docker/login-action-4
2 parents 0fb0395 + 44a7471 commit ff16c83

8 files changed

Lines changed: 234 additions & 62 deletions

File tree

.github/workflows/docker.yml

Lines changed: 49 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
1-
name: Release stable image
1+
name: Publish stable container images
22

33
on:
4-
push:
5-
branches:
6-
- "release/stable/**"
7-
pull_request:
8-
branches:
9-
- "release/stable/**"
10-
types: [opened, synchronize]
11-
12-
env:
13-
CARGO_TERM_COLOR: always
4+
release:
5+
types: [published]
6+
workflow_dispatch:
147

158
jobs:
16-
release_image:
9+
publish:
1710
strategy:
1811
fail-fast: false
1912
matrix:
@@ -23,57 +16,55 @@ jobs:
2316
- hybrid
2417
- no-cache
2518

26-
name: Release ${{ matrix.cache }} image
19+
name: Publish ${{ matrix.cache }} image
2720
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
packages: write
2824

2925
steps:
30-
- uses: actions/checkout@v6
31-
# Install buildx
26+
- name: Checkout
27+
uses: actions/checkout@v4
28+
29+
- name: Set up QEMU
30+
uses: docker/setup-qemu-action@v3
31+
3232
- name: Set up Docker Buildx
33-
id: buildx
34-
uses: docker/setup-buildx-action@v3
35-
# Set buildx cache
36-
- name: Cache register
37-
uses: actions/cache@v5
38-
with:
39-
path: /tmp/.buildx-cache
40-
key: buildx-cache
41-
# Login to ghcr.io
33+
uses: docker/setup-buildx-action@v4
34+
4235
- name: Log in to Docker Hub
4336
uses: docker/login-action@v4
44-
with:
37+
with:
4538
username: neonmmd
4639
password: ${{ secrets.DOCKERHUB_TOKEN }}
47-
# Extract branch info
48-
- name: Set info
49-
run: |
50-
echo "VERSION=$(echo ${GITHUB_REF} | awk -F/ '{print $6}')" >> $GITHUB_ENV
51-
# Print info for debug
52-
- name: Print Info
53-
run: |
54-
echo $VERSION
55-
# Create buildx multiarch
56-
- name: Create buildx multiarch
57-
run: docker buildx create --use --name=buildx-multi-arch --driver=docker-container --driver-opt=network=host
58-
# Modify cache variable in the dockerfile.
59-
- name: Modify Cache variable
60-
run: |
61-
sed -i "s/ARG CACHE=[a-z]*/ARG CACHE=${{ matrix.cache }}/g" Dockerfile
62-
# Publish image
63-
- name: Publish image
64-
run: docker buildx build --builder=buildx-multi-arch --platform=linux/amd64,linux/arm64 --build-arg CACHE=${{ matrix.cache }} --push -t neonmmd/websurfx:$VERSION-${{ matrix.cache }} -t neon-mmd/websurfx:${{matrix.cache}} -f Dockerfile .
65-
- name: Publish latest
66-
if: ${{ matrix.cache }} == 'hybrid'
67-
run: docker buildx build --builder=buildx-multi-arch --platform=linux/amd64,linux/arm64 --build-arg CACHE=${{ matrix.cache }} --push -t neon-mmd/websurfx:latest -f Dockerfile .
68-
# Upload it to release
69-
- name: Test if release already exists
70-
id: release-exists
71-
continue-on-error: true
72-
run: gh release view $BINARY_NAME-$VERSION
73-
env:
74-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75-
- name: Create new draft release
76-
if: steps.release-exists.outcome == 'failure' && steps.release-exists.conclusion == 'success'
77-
run: gh release create -t $VERSION -d $VERSION
78-
env:
79-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40+
41+
- name: Log in to GHCR
42+
uses: docker/login-action@v4
43+
with:
44+
registry: ghcr.io
45+
username: ${{ github.actor }}
46+
password: ${{ secrets.GITHUB_TOKEN }}
47+
48+
- name: Extract metadata
49+
id: meta
50+
uses: docker/metadata-action@v5
51+
with:
52+
images: |
53+
neonmmd/websurfx
54+
ghcr.io/${{ github.repository_owner }}/websurfx
55+
tags: |
56+
type=semver,pattern={{raw}},suffix=-${{ matrix.cache }}
57+
type=raw,value=latest,enable=${{ matrix.cache == 'memory' && !github.event.release.prerelease }}
58+
type=raw,value=${{ matrix.cache }},enable=true
59+
60+
- name: Build and push
61+
uses: docker/build-push-action@v6
62+
with:
63+
context: .
64+
platforms: linux/amd64,linux/arm64
65+
push: true
66+
tags: ${{ steps.meta.outputs.tags }}
67+
labels: ${{ steps.meta.outputs.labels }}
68+
cache-from: type=gha,scope=${{ matrix.cache }}
69+
cache-to: type=gha,mode=max,scope=${{ matrix.cache }}
70+
build-args: CACHE=${{ matrix.cache }}

.github/workflows/hello.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
contents: read
1515
pull-requests: write
1616
steps:
17-
- uses: actions/first-interaction@2c1a690e6b60f93eb33eb4a15b9a12afbb4f2f21 # v3.1.0
17+
- uses: actions/first-interaction@v3
1818
with:
1919
repo-token: ${{ secrets.GITHUB_TOKEN }}
2020
pr-message: |-

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ name = "websurfx"
1111
path = "src/main.rs"
1212

1313
[dependencies]
14+
form_urlencoded = "1.2"
1415
rayon = "1.10.0"
1516
arc-swap = "1.9.0"
1617
reqwest = { version = "0.12.19", default-features = false, features = [
@@ -85,7 +86,7 @@ keyword_extraction = { version = "1.5.0", default-features = false, features = [
8586
"tf_idf",
8687
"rayon",
8788
] }
88-
stop-words = { version = "0.9.0", default-features = false, features = ["iso"] }
89+
stop-words = { version = "0.10.0", default-features = false, features = ["iso"] }
8990
thesaurus = { version = "0.5.2", default-features = false, optional = true, features = [
9091
"moby",
9192
]}

src/engines/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub mod librex;
1010
pub mod mojeek;
1111
mod search_result_parser;
1212
pub mod searx;
13+
pub mod sepiasearch;
1314
pub mod startpage;
1415
pub mod wikipedia;
1516
pub mod yahoo;

src/engines/sepiasearch.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
//! The `sepiasearch` module handles the fetching of results from the SepiaSearch video search
2+
//! engine (PeerTube search index) by querying its JSON API with the user provided query.
3+
4+
use reqwest::{Client, header::HeaderMap};
5+
use serde::Deserialize;
6+
use std::collections::HashMap;
7+
8+
use crate::models::aggregation::SearchResult;
9+
use crate::models::engine::{EngineError, SearchEngine};
10+
use error_stack::{Report, Result, ResultExt};
11+
12+
/// A new SepiaSearch engine type defined in-order to implement the `SearchEngine` trait.
13+
pub struct SepiaSearch;
14+
15+
/// The JSON response structure returned by the SepiaSearch API.
16+
#[derive(Deserialize)]
17+
struct SepiaSearchResponse {
18+
/// The list of video results returned by the API.
19+
data: Vec<SepiaSearchVideo>,
20+
/// The total number of results available.
21+
total: Option<u32>,
22+
/// An error message, if the API returned one.
23+
error: Option<String>,
24+
}
25+
26+
/// A single video result from the SepiaSearch API.
27+
#[derive(Deserialize)]
28+
struct SepiaSearchVideo {
29+
/// The title of the video.
30+
name: String,
31+
/// The URL to watch the video.
32+
url: String,
33+
/// An optional description of the video.
34+
description: Option<String>,
35+
}
36+
37+
impl SepiaSearch {
38+
/// Creates a new SepiaSearch engine instance.
39+
pub fn new() -> Result<SepiaSearch, EngineError> {
40+
Ok(Self)
41+
}
42+
43+
/// Parses the raw JSON response body into a list of search results.
44+
fn parse_json_response(json: &[u8]) -> Result<Vec<(String, SearchResult)>, EngineError> {
45+
let response: SepiaSearchResponse =
46+
serde_json::from_slice(json).change_context(EngineError::UnexpectedError)?;
47+
48+
if let Some(err) = &response.error {
49+
return Err(Report::new(EngineError::UnexpectedError)
50+
.attach(format!("SepiaSearch API error: {err}")));
51+
}
52+
53+
let results = response
54+
.data
55+
.into_iter()
56+
.map(|video| {
57+
let description = video.description.unwrap_or_default().trim().to_string();
58+
let search_result = SearchResult::new(
59+
video.name.trim(),
60+
video.url.as_str(),
61+
description.as_str(),
62+
&["sepiasearch"],
63+
);
64+
(search_result.url.clone(), search_result)
65+
})
66+
.collect();
67+
68+
Ok(results)
69+
}
70+
}
71+
72+
#[async_trait::async_trait]
73+
impl SearchEngine for SepiaSearch {
74+
async fn results(
75+
&self,
76+
query: &str,
77+
page: u32,
78+
user_agent: &str,
79+
client: &Client,
80+
safe_search: u8,
81+
) -> Result<Vec<(String, SearchResult)>, EngineError> {
82+
let nsfw = if safe_search == 0 { "both" } else { "false" };
83+
84+
// Pagination: 0-based offset, 10 results per page
85+
let start = page * 10;
86+
87+
let encoded_query = form_urlencoded::byte_serialize(query.as_bytes()).collect::<String>();
88+
let url = format!(
89+
"https://sepiasearch.org/api/v1/search/videos?search={encoded_query}&start={start}&count=10&sort=-match&nsfw={nsfw}"
90+
);
91+
92+
let header_map = HeaderMap::try_from(&HashMap::from([
93+
("User-Agent".to_string(), user_agent.to_string()),
94+
(
95+
"Referer".to_string(),
96+
"https://sepiasearch.org/".to_string(),
97+
),
98+
(
99+
"Content-Type".to_string(),
100+
"text/html; charset=utf-8".to_string(),
101+
),
102+
]))
103+
.change_context(EngineError::UnexpectedError)?;
104+
105+
let json_bytes =
106+
SepiaSearch::fetch_json_as_bytes_from_upstream(self, &url, header_map, client).await?;
107+
108+
let results = Self::parse_json_response(&json_bytes)?;
109+
110+
if results.is_empty() {
111+
return Err(Report::new(EngineError::EmptyResultSet));
112+
}
113+
114+
Ok(results)
115+
}
116+
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
122+
#[test]
123+
fn test_parse_json_response() {
124+
let json = br#"{
125+
"total": 1,
126+
"data": [
127+
{
128+
"name": "Test Video",
129+
"url": "https://video.example.org/videos/watch/abc123",
130+
"description": "A test video description"
131+
}
132+
]
133+
}"#;
134+
135+
let results = SepiaSearch::parse_json_response(json).unwrap();
136+
assert_eq!(results.len(), 1);
137+
assert_eq!(results[0].1.title, "Test Video");
138+
assert_eq!(
139+
results[0].1.url,
140+
"https://video.example.org/videos/watch/abc123"
141+
);
142+
assert_eq!(results[0].1.description, "A test video description");
143+
assert_eq!(results[0].1.engine, vec!["sepiasearch"]);
144+
}
145+
146+
#[test]
147+
fn test_parse_json_response_no_description() {
148+
let json = br#"{
149+
"total": 1,
150+
"data": [
151+
{
152+
"name": "No Desc Video",
153+
"url": "https://video.example.org/videos/watch/def456"
154+
}
155+
]
156+
}"#;
157+
158+
let results = SepiaSearch::parse_json_response(json).unwrap();
159+
assert_eq!(results.len(), 1);
160+
assert_eq!(results[0].1.description, "");
161+
}
162+
163+
#[test]
164+
fn test_parse_json_response_empty_results() {
165+
let json = br#"{
166+
"total": 0,
167+
"data": []
168+
}"#;
169+
170+
let results = SepiaSearch::parse_json_response(json).unwrap();
171+
assert!(results.is_empty());
172+
}
173+
}

src/models/engine.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ impl EngineHandler {
214214
let engine = crate::engines::yahoo::Yahoo::new()?;
215215
("yahoo", Box::new(engine))
216216
}
217+
"sepiasearch" => {
218+
let engine = crate::engines::sepiasearch::SepiaSearch::new()?;
219+
("sepiasearch", Box::new(engine))
220+
}
217221
_ => {
218222
return Err(Report::from(EngineError::NoSuchEngineFound(
219223
engine_name.to_string(),

websurfx/config.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ upstream_search_engines = {
8080
Bing = false,
8181
Wikipedia = true,
8282
Yahoo = false,
83+
SepiaSearch = false,
8384
} -- select the upstream search engines from which the results should be fetched.
8485

8586
proxy = nil -- Proxy to send outgoing requests through. Set to nil to disable.

0 commit comments

Comments
 (0)