Compare commits
No commits in common. "main" and "indexing" have entirely different histories.
115 changed files with 1026 additions and 4481 deletions
|
@ -1,144 +0,0 @@
|
|||
name: QGato CLI Release Build
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: debian
|
||||
|
||||
steps:
|
||||
- name: Checkout source
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check Go version
|
||||
run: |
|
||||
go version
|
||||
|
||||
- name: Extract version from version.txt
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(cat version.txt)
|
||||
VERSION="v${VERSION#v}"
|
||||
echo "$VERSION" > version.txt
|
||||
echo "✅ Detected version: $VERSION"
|
||||
|
||||
- name: Build all targets
|
||||
run: |
|
||||
mkdir -p bundles
|
||||
|
||||
PLATFORMS=(
|
||||
"linux/amd64"
|
||||
"linux/arm64"
|
||||
"linux/arm/v7"
|
||||
"linux/arm/v6"
|
||||
"linux/riscv64"
|
||||
"windows/amd64"
|
||||
"windows/arm64"
|
||||
)
|
||||
|
||||
for TARGET in "${PLATFORMS[@]}"; do
|
||||
OS=$(echo "$TARGET" | cut -d/ -f1)
|
||||
ARCH=$(echo "$TARGET" | cut -d/ -f2)
|
||||
VARIANT=$(echo "$TARGET" | cut -d/ -f3)
|
||||
|
||||
OUT="qgato-${OS}-${ARCH}"
|
||||
[ -n "$VARIANT" ] && OUT="${OUT}${VARIANT}"
|
||||
BIN="$OUT"
|
||||
[ "$OS" = "windows" ] && BIN="${OUT}.exe"
|
||||
|
||||
echo "🔨 Building $BIN"
|
||||
|
||||
# Disable CGO for cross-compiled targets (everything except native linux/amd64)
|
||||
if [ "$TARGET" = "linux/amd64" ]; then
|
||||
export CGO_ENABLED=1
|
||||
else
|
||||
export CGO_ENABLED=0
|
||||
fi
|
||||
|
||||
if [ "$ARCH" = "arm" ]; then
|
||||
case "$VARIANT" in
|
||||
v7) GOARM=7 ;;
|
||||
v6) GOARM=6 ;;
|
||||
*) GOARM=7 ;;
|
||||
esac
|
||||
GOOS=$OS GOARCH=arm GOARM=$GOARM \
|
||||
go build -ldflags="-s -w" -o "$BIN" ./.
|
||||
else
|
||||
GOOS=$OS GOARCH=$ARCH \
|
||||
go build -ldflags="-s -w" -o "$BIN" ./.
|
||||
fi
|
||||
|
||||
echo "📦 Packaging $BIN with required files..."
|
||||
|
||||
PKG_DIR="bundle-$OUT"
|
||||
mkdir "$PKG_DIR"
|
||||
cp "$BIN" "$PKG_DIR/"
|
||||
cp -r lang static templates config.ini "$PKG_DIR/" 2>/dev/null || true
|
||||
|
||||
if [ "$OS" = "windows" ]; then
|
||||
zip -r "bundles/$OUT.zip" "$PKG_DIR"
|
||||
else
|
||||
tar -czf "bundles/$OUT.tar.gz" "$PKG_DIR"
|
||||
fi
|
||||
|
||||
rm -rf "$PKG_DIR" "$BIN"
|
||||
done
|
||||
|
||||
- name: Create Forgejo release
|
||||
run: |
|
||||
TAG_NAME=$(cat version.txt)
|
||||
echo "📦 Creating release for tag: $TAG_NAME"
|
||||
|
||||
DOWNLOAD_BASE="https://weforge.xyz/spitfire/Search/releases/download/$TAG_NAME"
|
||||
|
||||
echo "| Arch | Linux Bundle (.tar.gz) | Windows Bundle (.zip) |" > release.md
|
||||
echo "|---------|---------------------------------------------------|--------------------------------------------------|" >> release.md
|
||||
echo "| amd64 | [qgato-linux-amd64.tar.gz]($DOWNLOAD_BASE/qgato-linux-amd64.tar.gz) | [qgato-windows-amd64.zip]($DOWNLOAD_BASE/qgato-windows-amd64.zip) |" >> release.md
|
||||
echo "| arm64 | [qgato-linux-arm64.tar.gz]($DOWNLOAD_BASE/qgato-linux-arm64.tar.gz) | [qgato-windows-arm64.zip]($DOWNLOAD_BASE/qgato-windows-arm64.zip) |" >> release.md
|
||||
echo "| armv7 | [qgato-linux-armv7.tar.gz]($DOWNLOAD_BASE/qgato-linux-armv7.tar.gz) | — |" >> release.md
|
||||
echo "| armv6 | [qgato-linux-armv6.tar.gz]($DOWNLOAD_BASE/qgato-linux-armv6.tar.gz) | — |" >> release.md
|
||||
echo "| riscv64 | [qgato-linux-riscv64.tar.gz]($DOWNLOAD_BASE/qgato-linux-riscv64.tar.gz) | — |" >> release.md
|
||||
|
||||
RELEASE_BODY=$(cat release.md | jq -Rs .)
|
||||
|
||||
curl -sSL -X POST "$FORGEJO_API/repos/${OWNER}/${REPO}/releases" \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"tag_name\": \"$TAG_NAME\",
|
||||
\"name\": \"$TAG_NAME\",
|
||||
\"body\": $RELEASE_BODY,
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}"
|
||||
env:
|
||||
FORGEJO_API: https://weforge.xyz/api/v1
|
||||
OWNER: spitfire
|
||||
REPO: Search
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||
|
||||
- name: Upload all bundles
|
||||
run: |
|
||||
TAG_NAME=$(cat version.txt)
|
||||
RELEASE_ID=$(curl -s -H "Authorization: token $FORGEJO_TOKEN" \
|
||||
"$FORGEJO_API/repos/${OWNER}/${REPO}/releases/tags/$TAG_NAME" | jq -r .id)
|
||||
|
||||
for FILE in bundles/*; do
|
||||
NAME=$(basename "$FILE")
|
||||
echo "📤 Uploading $NAME"
|
||||
|
||||
CONTENT_TYPE="application/octet-stream"
|
||||
[[ "$FILE" == *.zip ]] && CONTENT_TYPE="application/zip"
|
||||
[[ "$FILE" == *.tar.gz ]] && CONTENT_TYPE="application/gzip"
|
||||
|
||||
curl -sSL -X POST "$FORGEJO_API/repos/${OWNER}/${REPO}/releases/${RELEASE_ID}/assets?name=$NAME" \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" \
|
||||
-H "Content-Type: $CONTENT_TYPE" \
|
||||
--data-binary "@$FILE"
|
||||
done
|
||||
env:
|
||||
FORGEJO_API: https://weforge.xyz/api/v1
|
||||
OWNER: spitfire
|
||||
REPO: Search
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,5 +6,4 @@ cache/
|
|||
*.min.js
|
||||
*.min.css
|
||||
qgato
|
||||
qgato.exe
|
||||
test.py
|
24
README.md
24
README.md
|
@ -47,11 +47,11 @@ A self-hosted private search engine designed to be scalable and more resource-ef
|
|||
|
||||
### For Self-Hosting
|
||||
|
||||
- **[Easy to Set Up](https://weforge.xyz/Spitfire/Search/wiki/Setup-Other)** - Quick and straightforward setup process for anyone.
|
||||
- **Self-hosted option** - Run on your own server for even more privacy.
|
||||
- **Lightweight** - Low memory footprint (15-30MiB) even during searches.
|
||||
- **Decentralized** - No single point of failure.
|
||||
- **Results caching in RAM** - Faster response times through caching.
|
||||
- **[Configurable](https://weforge.xyz/Spitfire/Search/wiki/Config)** - Fully customizable via the `config.ini` file.
|
||||
- **Configurable** - Tweak features via `config.ini`.
|
||||
- **Flexible media support** - Images optionally stored on HDD/SSD for caching and improved response time.
|
||||
|
||||
### Results Sources
|
||||
|
@ -73,20 +73,30 @@ A self-hosted private search engine designed to be scalable and more resource-ef
|
|||
|
||||
### Running the QGato
|
||||
|
||||
Linux:
|
||||
|
||||
```bash
|
||||
git clone https://weforge.xyz/Spitfire/Search.git
|
||||
cd Search
|
||||
go run .
|
||||
chmod +x ./run.sh
|
||||
./run.sh
|
||||
```
|
||||
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
git clone https://weforge.xyz/Spitfire/Search.git
|
||||
cd Search
|
||||
.\run.bat
|
||||
```
|
||||
|
||||
*Its that easy!*
|
||||
|
||||
### Configuring
|
||||
|
||||
- Configuration is done via the `config.ini` file.
|
||||
- On first start, you will be guided through the basic setup.
|
||||
- For more advanced configuration options, visit the [Wiki Configuration Page](https://weforge.xyz/Spitfire/Search/wiki/Config).
|
||||
|
||||
Configuration is done via the ``config.ini`` file.
|
||||
On first start, you will be guided through the basic setup.
|
||||
More advanced setup and all options will be listed here later, as this is still being updated.
|
||||
|
||||
## License
|
||||
|
||||
|
|
162
agent.go
162
agent.go
|
@ -11,13 +11,11 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// BrowserVersion represents the version & global usage from the caniuse data
|
||||
type BrowserVersion struct {
|
||||
Version string `json:"version"`
|
||||
Global float64 `json:"global"`
|
||||
}
|
||||
|
||||
// BrowserData holds sets of versions for Firefox and Chromium
|
||||
type BrowserData struct {
|
||||
Firefox []BrowserVersion `json:"firefox"`
|
||||
Chromium []BrowserVersion `json:"chrome"`
|
||||
|
@ -30,7 +28,6 @@ var (
|
|||
}{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
|
||||
browserCache = struct {
|
||||
sync.RWMutex
|
||||
data BrowserData
|
||||
|
@ -40,19 +37,26 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
// fetchLatestBrowserVersions retrieves usage data from caniuse.com’s fulldata JSON.
|
||||
func fetchLatestBrowserVersions() (BrowserData, error) {
|
||||
const urlCaniuse = "https://raw.githubusercontent.com/Fyrd/caniuse/master/fulldata-json/data-2.0.json"
|
||||
url := "https://raw.githubusercontent.com/Fyrd/caniuse/master/fulldata-json/data-2.0.json"
|
||||
|
||||
// // Optional: skip TLS verification to avoid certificate errors
|
||||
// transport := &http.Transport{
|
||||
// TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
// }
|
||||
|
||||
// Increase the HTTP client timeout
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
// Transport: transport,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", urlCaniuse, nil)
|
||||
// Build the request manually to set headers
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return BrowserData{}, err
|
||||
}
|
||||
|
||||
// Set a simple custom User-Agent and language
|
||||
// Custom user agent and English language preference
|
||||
req.Header.Set("User-Agent", "MyCustomAgent/1.0 (compatible; +https://example.com)")
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
|
||||
|
@ -67,42 +71,36 @@ func fetchLatestBrowserVersions() (BrowserData, error) {
|
|||
return BrowserData{}, err
|
||||
}
|
||||
|
||||
var rawData map[string]any
|
||||
var rawData map[string]interface{}
|
||||
if err := json.Unmarshal(body, &rawData); err != nil {
|
||||
return BrowserData{}, err
|
||||
}
|
||||
|
||||
stats, ok := rawData["agents"].(map[string]any)
|
||||
if !ok {
|
||||
return BrowserData{}, fmt.Errorf("unexpected JSON structure (no 'agents' field)")
|
||||
}
|
||||
stats := rawData["agents"].(map[string]interface{})
|
||||
|
||||
var data BrowserData
|
||||
|
||||
// Extract Firefox data
|
||||
if firefoxData, ok := stats["firefox"].(map[string]any); ok {
|
||||
if usageMap, ok := firefoxData["usage_global"].(map[string]any); ok {
|
||||
for version, usage := range usageMap {
|
||||
val, _ := usage.(float64)
|
||||
data.Firefox = append(data.Firefox, BrowserVersion{Version: version, Global: val})
|
||||
}
|
||||
if firefoxData, ok := stats["firefox"].(map[string]interface{}); ok {
|
||||
for version, usage := range firefoxData["usage_global"].(map[string]interface{}) {
|
||||
data.Firefox = append(data.Firefox, BrowserVersion{
|
||||
Version: version,
|
||||
Global: usage.(float64),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Extract Chrome data
|
||||
if chromeData, ok := stats["chrome"].(map[string]any); ok {
|
||||
if usageMap, ok := chromeData["usage_global"].(map[string]any); ok {
|
||||
for version, usage := range usageMap {
|
||||
val, _ := usage.(float64)
|
||||
data.Chromium = append(data.Chromium, BrowserVersion{Version: version, Global: val})
|
||||
}
|
||||
if chromeData, ok := stats["chrome"].(map[string]interface{}); ok {
|
||||
for version, usage := range chromeData["usage_global"].(map[string]interface{}) {
|
||||
data.Chromium = append(data.Chromium, BrowserVersion{
|
||||
Version: version,
|
||||
Global: usage.(float64),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// getLatestBrowserVersions checks the cache and fetches new data if expired
|
||||
func getLatestBrowserVersions() (BrowserData, error) {
|
||||
browserCache.RLock()
|
||||
if time.Now().Before(browserCache.expires) {
|
||||
|
@ -119,36 +117,37 @@ func getLatestBrowserVersions() (BrowserData, error) {
|
|||
|
||||
browserCache.Lock()
|
||||
browserCache.data = data
|
||||
browserCache.expires = time.Now().Add(24 * time.Hour) // Refresh daily
|
||||
browserCache.expires = time.Now().Add(24 * time.Hour)
|
||||
browserCache.Unlock()
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// randomUserAgent picks a random browser (Firefox/Chromium), selects a version based on usage,
|
||||
// picks an OS string, and composes a User-Agent header.
|
||||
func randomUserAgent() (string, error) {
|
||||
browsers, err := getLatestBrowserVersions()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// Overall usage: 80% chance for Chromium, 20% for Firefox
|
||||
// Simulated browser usage statistics (in percentages)
|
||||
usageStats := map[string]float64{
|
||||
"Firefox": 20.0,
|
||||
"Chromium": 80.0,
|
||||
"Firefox": 30.0,
|
||||
"Chromium": 70.0,
|
||||
}
|
||||
|
||||
// Weighted random selection of the browser type
|
||||
// Calculate the probabilities for the versions
|
||||
probabilities := []float64{0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125, 0.00390625}
|
||||
|
||||
// Select a browser based on usage statistics
|
||||
browserType := ""
|
||||
randVal := r.Float64() * 100
|
||||
randVal := rand.Float64() * 100
|
||||
cumulative := 0.0
|
||||
for bType, usage := range usageStats {
|
||||
for browser, usage := range usageStats {
|
||||
cumulative += usage
|
||||
if randVal < cumulative {
|
||||
browserType = bType
|
||||
browserType = browser
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -165,16 +164,14 @@ func randomUserAgent() (string, error) {
|
|||
return "", fmt.Errorf("no versions found for browser: %s", browserType)
|
||||
}
|
||||
|
||||
// Sort by global usage descending
|
||||
// Sort versions by usage (descending order)
|
||||
sort.Slice(versions, func(i, j int) bool {
|
||||
return versions[i].Global > versions[j].Global
|
||||
})
|
||||
|
||||
// Probability distribution for top few versions
|
||||
probabilities := []float64{0.5, 0.25, 0.125, 0.0625, 0.03125, 0.015625, 0.0078125, 0.00390625}
|
||||
|
||||
// Select a version based on the probabilities
|
||||
version := ""
|
||||
randVal = r.Float64()
|
||||
randVal = rand.Float64()
|
||||
cumulative = 0.0
|
||||
for i, p := range probabilities {
|
||||
cumulative += p
|
||||
|
@ -184,72 +181,68 @@ func randomUserAgent() (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Fallback to the least used version if none matched
|
||||
// Fallback to the last version if none matched
|
||||
if version == "" {
|
||||
version = versions[len(versions)-1].Version
|
||||
}
|
||||
|
||||
userAgent := generateUserAgent(browserType, version, r)
|
||||
// Generate the user agent string
|
||||
userAgent := generateUserAgent(browserType, version)
|
||||
return userAgent, nil
|
||||
}
|
||||
|
||||
// generateUserAgent composes the final UA string given the browser, version, and OS.
|
||||
func generateUserAgent(browser, version string, r *rand.Rand) string {
|
||||
func generateUserAgent(browser, version string) string {
|
||||
oses := []struct {
|
||||
os string
|
||||
probability float64
|
||||
}{
|
||||
{"Windows NT 10.0; Win64; x64", 44.0},
|
||||
{"X11; Linux x86_64", 2.0},
|
||||
{"X11; Ubuntu; Linux x86_64", 2.0},
|
||||
{"Windows NT 11.0; Win64; x64", 44.0},
|
||||
{"X11; Linux x86_64", 1.0},
|
||||
{"X11; Ubuntu; Linux x86_64", 1.0},
|
||||
{"Macintosh; Intel Mac OS X 10_15_7", 10.0},
|
||||
}
|
||||
|
||||
// Weighted random selection for OS
|
||||
randVal := r.Float64() * 100
|
||||
// Select an OS based on probabilities
|
||||
randVal := rand.Float64() * 100
|
||||
cumulative := 0.0
|
||||
selectedOS := oses[0].os // Default in case distribution is off
|
||||
for _, entry := range oses {
|
||||
cumulative += entry.probability
|
||||
selectedOS := ""
|
||||
for _, os := range oses {
|
||||
cumulative += os.probability
|
||||
if randVal < cumulative {
|
||||
selectedOS = entry.os
|
||||
selectedOS = os.os
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
switch browser {
|
||||
case "Firefox":
|
||||
// Example: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:117.0) Gecko/20100101 Firefox/117.0
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s) Gecko/20100101 Firefox/%s", selectedOS, version, version)
|
||||
case "Chromium":
|
||||
// Example: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", selectedOS, version)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// updateCachedUserAgents randomly updates half of the cached UAs to new versions
|
||||
func updateCachedUserAgents(newVersions BrowserData) {
|
||||
cache.Lock()
|
||||
defer cache.Unlock()
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for key, userAgent := range cache.data {
|
||||
if r.Float64() < 0.5 {
|
||||
updatedUserAgent := updateUserAgentVersion(userAgent, newVersions, r)
|
||||
randVal := rand.Float64()
|
||||
if randVal < 0.5 {
|
||||
updatedUserAgent := updateUserAgentVersion(userAgent, newVersions)
|
||||
cache.data[key] = updatedUserAgent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateUserAgentVersion tries to parse the old UA, detect its browser, and update the version
|
||||
func updateUserAgentVersion(userAgent string, newVersions BrowserData, r *rand.Rand) string {
|
||||
func updateUserAgentVersion(userAgent string, newVersions BrowserData) string {
|
||||
// Parse the current user agent to extract browser and version
|
||||
var browserType, version string
|
||||
|
||||
// Attempt to detect old UA patterns (Chromium or Firefox)
|
||||
if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
|
||||
browserType = "Chromium"
|
||||
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
|
||||
browserType = "Chromium"
|
||||
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
|
||||
browserType = "Chromium"
|
||||
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", &version); err == nil {
|
||||
|
@ -258,6 +251,8 @@ func updateUserAgentVersion(userAgent string, newVersions BrowserData, r *rand.R
|
|||
browserType = "Chromium"
|
||||
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
|
||||
browserType = "Firefox"
|
||||
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (Windows NT 11.0; Win64; x64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
|
||||
browserType = "Firefox"
|
||||
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Linux x86_64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
|
||||
browserType = "Firefox"
|
||||
} else if _, err := fmt.Sscanf(userAgent, "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:%s) Gecko/20100101 Firefox/%s", &version, &version); err == nil {
|
||||
|
@ -266,37 +261,22 @@ func updateUserAgentVersion(userAgent string, newVersions BrowserData, r *rand.R
|
|||
browserType = "Firefox"
|
||||
}
|
||||
|
||||
// Grab the newest version from the fetched data
|
||||
// Get the latest version for that browser
|
||||
var latestVersion string
|
||||
if browserType == "Firefox" && len(newVersions.Firefox) > 0 {
|
||||
// Sort by usage descending
|
||||
sort.Slice(newVersions.Firefox, func(i, j int) bool {
|
||||
return newVersions.Firefox[i].Global > newVersions.Firefox[j].Global
|
||||
})
|
||||
latestVersion = newVersions.Firefox[0].Version
|
||||
} else if browserType == "Chromium" && len(newVersions.Chromium) > 0 {
|
||||
// Sort by usage descending
|
||||
sort.Slice(newVersions.Chromium, func(i, j int) bool {
|
||||
return newVersions.Chromium[i].Global > newVersions.Chromium[j].Global
|
||||
})
|
||||
latestVersion = newVersions.Chromium[0].Version
|
||||
}
|
||||
|
||||
// If we failed to detect the browser or have no data, just return the old UA
|
||||
if browserType == "" || latestVersion == "" {
|
||||
return userAgent
|
||||
}
|
||||
|
||||
// Create a new random OS-based UA string with the latest version
|
||||
return generateUserAgent(browserType, latestVersion, r)
|
||||
// Update the user agent string with the new version
|
||||
return generateUserAgent(browserType, latestVersion)
|
||||
}
|
||||
|
||||
// periodicAgentUpdate periodically refreshes browser data and user agents
|
||||
func periodicAgentUpdate() {
|
||||
for {
|
||||
// Sleep a random interval between 1 and 2 days
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
time.Sleep(time.Duration(24+r.Intn(24)) * time.Hour)
|
||||
// Sleep for a random interval between 1 and 2 days
|
||||
time.Sleep(time.Duration(24+rand.Intn(24)) * time.Hour)
|
||||
|
||||
// Fetch the latest browser versions
|
||||
newVersions, err := fetchLatestBrowserVersions()
|
||||
|
@ -316,7 +296,6 @@ func periodicAgentUpdate() {
|
|||
}
|
||||
}
|
||||
|
||||
// GetUserAgent returns a cached UA for the given key or creates one if none exists.
|
||||
func GetUserAgent(cacheKey string) (string, error) {
|
||||
cache.RLock()
|
||||
userAgent, found := cache.data[cacheKey]
|
||||
|
@ -335,11 +314,9 @@ func GetUserAgent(cacheKey string) (string, error) {
|
|||
cache.data[cacheKey] = userAgent
|
||||
cache.Unlock()
|
||||
|
||||
printDebug("Generated (cached or new) user agent: %s", userAgent)
|
||||
return userAgent, nil
|
||||
}
|
||||
|
||||
// GetNewUserAgent always returns a newly generated UA, overwriting the cache.
|
||||
func GetNewUserAgent(cacheKey string) (string, error) {
|
||||
userAgent, err := randomUserAgent()
|
||||
if err != nil {
|
||||
|
@ -350,7 +327,6 @@ func GetNewUserAgent(cacheKey string) (string, error) {
|
|||
cache.data[cacheKey] = userAgent
|
||||
cache.Unlock()
|
||||
|
||||
printDebug("Generated new user agent: %s", userAgent)
|
||||
return userAgent, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/fyne-io/image/ico"
|
||||
"golang.org/x/image/bmp"
|
||||
"golang.org/x/image/tiff"
|
||||
)
|
||||
|
@ -36,7 +35,7 @@ var (
|
|||
imageURLMapMu sync.RWMutex
|
||||
)
|
||||
|
||||
func cacheImage(imageURL, imageID string, imageType string) (string, bool, error) {
|
||||
func cacheImage(imageURL, imageID string, isThumbnail bool) (string, bool, error) {
|
||||
if imageURL == "" {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, fmt.Errorf("empty image URL for image ID %s", imageID)
|
||||
|
@ -44,15 +43,10 @@ func cacheImage(imageURL, imageID string, imageType string) (string, bool, error
|
|||
|
||||
// Construct the filename based on the image ID and type
|
||||
var filename string
|
||||
switch imageType {
|
||||
case "thumb":
|
||||
if isThumbnail {
|
||||
filename = fmt.Sprintf("%s_thumb.webp", imageID)
|
||||
case "icon":
|
||||
filename = fmt.Sprintf("%s_icon.webp", imageID)
|
||||
case "full":
|
||||
} else {
|
||||
filename = fmt.Sprintf("%s_full.webp", imageID)
|
||||
default:
|
||||
return "", false, fmt.Errorf("unknown image type: %s", imageType)
|
||||
}
|
||||
|
||||
// Make sure we store inside: config.DriveCache.Path / images
|
||||
|
@ -145,8 +139,6 @@ func cacheImage(imageURL, imageID string, imageType string) (string, bool, error
|
|||
// Decode the image based on the content type
|
||||
var img image.Image
|
||||
switch contentType {
|
||||
case "image/x-icon", "image/vnd.microsoft.icon":
|
||||
img, err = ico.Decode(bytes.NewReader(data))
|
||||
case "image/jpeg":
|
||||
img, err = jpeg.Decode(bytes.NewReader(data))
|
||||
case "image/png":
|
||||
|
@ -233,23 +225,29 @@ func handleImageServe(w http.ResponseWriter, r *http.Request) {
|
|||
// Adjust to read from config.DriveCache.Path / images
|
||||
cachedImagePath := filepath.Join(config.DriveCache.Path, "images", filename)
|
||||
|
||||
if hasExtension && (imageType == "thumb" || imageType == "icon") {
|
||||
if hasExtension && imageType == "thumb" {
|
||||
// Requesting cached image (thumbnail or full)
|
||||
if _, err := os.Stat(cachedImagePath); err == nil {
|
||||
// Update the modification time
|
||||
_ = os.Chtimes(cachedImagePath, time.Now(), time.Now())
|
||||
w.Header().Set("Content-Type", "image/webp")
|
||||
// Update the modification time to now
|
||||
err := os.Chtimes(cachedImagePath, time.Now(), time.Now())
|
||||
if err != nil {
|
||||
printWarn("Failed to update modification time for %s: %v", cachedImagePath, err)
|
||||
}
|
||||
|
||||
// Determine content type based on file extension
|
||||
contentType := "image/webp"
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
http.ServeFile(w, r, cachedImagePath)
|
||||
return
|
||||
} else {
|
||||
// Cached image not found
|
||||
if config.DriveCacheEnabled {
|
||||
if imageType == "icon" {
|
||||
serveGlobeImage(w, r)
|
||||
} else {
|
||||
serveMissingImage(w, r)
|
||||
}
|
||||
// Thumbnail should be cached, but not found
|
||||
serveMissingImage(w, r)
|
||||
return
|
||||
}
|
||||
// Else, proceed to proxy if caching is disabled
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,12 +323,8 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) {
|
|||
invalidImageIDsMu.Unlock()
|
||||
|
||||
if isInvalid {
|
||||
// Image is invalid; provide appropriate fallback
|
||||
if strings.HasSuffix(id, "_icon.webp") || strings.HasSuffix(id, "_icon") {
|
||||
statusMap[id] = "/images/globe.svg"
|
||||
} else {
|
||||
statusMap[id] = "/images/missing.svg"
|
||||
}
|
||||
// Image is invalid; inform the frontend by setting the missing image URL
|
||||
statusMap[id] = "/static/images/missing.svg"
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -338,15 +332,11 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) {
|
|||
extensions := []string{"webp", "svg"} // Extensions without leading dots
|
||||
imageReady := false
|
||||
|
||||
// Check thumbnail first
|
||||
for _, ext := range extensions {
|
||||
thumbPath := filepath.Join(config.DriveCache.Path, "images", fmt.Sprintf("%s_thumb.%s", id, ext))
|
||||
iconPath := filepath.Join(config.DriveCache.Path, "images", fmt.Sprintf("%s_icon.%s", id, ext))
|
||||
thumbFilename := fmt.Sprintf("%s_thumb.%s", id, ext)
|
||||
thumbPath := filepath.Join(config.DriveCache.Path, "images", thumbFilename)
|
||||
|
||||
if _, err := os.Stat(iconPath); err == nil {
|
||||
statusMap[id] = fmt.Sprintf("/image/%s_icon.%s", id, ext)
|
||||
imageReady = true
|
||||
break
|
||||
}
|
||||
if _, err := os.Stat(thumbPath); err == nil {
|
||||
statusMap[id] = fmt.Sprintf("/image/%s_thumb.%s", id, ext)
|
||||
imageReady = true
|
||||
|
@ -370,13 +360,11 @@ func handleImageStatus(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// If neither is ready and image is not invalid
|
||||
if !imageReady {
|
||||
// Distinguish favicon vs image fallback
|
||||
if strings.HasSuffix(id, "_icon.webp") || strings.HasSuffix(id, "_icon") {
|
||||
statusMap[id] = "/images/globe.svg"
|
||||
} else if !config.DriveCacheEnabled {
|
||||
statusMap[id] = "/images/missing.svg"
|
||||
if !config.DriveCacheEnabled {
|
||||
// Hard cache is disabled; use the proxy URL
|
||||
statusMap[id] = fmt.Sprintf("/image/%s_thumb", id)
|
||||
}
|
||||
// else: leave it unset — frontend will retry
|
||||
// Else, do not set statusMap[id]; the frontend will keep checking
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -529,25 +517,8 @@ func serveMissingImage(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set("Cache-Control", "no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
if config.DriveCacheEnabled {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
http.ServeFile(w, r, missingImagePath)
|
||||
}
|
||||
|
||||
func serveGlobeImage(w http.ResponseWriter, r *http.Request) {
|
||||
globePath := filepath.Join("static", "images", "globe.svg")
|
||||
|
||||
// Set error code FIRST
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
// Now read the file and write it manually, to avoid conflict with http.ServeFile
|
||||
data, err := os.ReadFile(globePath)
|
||||
if err != nil {
|
||||
http.Error(w, "globe.svg not found", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/svg+xml")
|
||||
w.Header().Set("Cache-Control", "no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
|
51
cache.go
51
cache.go
|
@ -62,18 +62,6 @@ type ForumSearchResult struct {
|
|||
ThumbnailSrc string `json:"thumbnailSrc,omitempty"`
|
||||
}
|
||||
|
||||
type MusicResult struct {
|
||||
URL string
|
||||
Title string
|
||||
Artist string
|
||||
Description string
|
||||
PublishedDate string
|
||||
Thumbnail string
|
||||
// AudioURL string
|
||||
Source string
|
||||
Duration string
|
||||
}
|
||||
|
||||
// GeocodeCachedItem represents a geocoding result stored in the cache.
|
||||
type GeocodeCachedItem struct {
|
||||
Latitude string
|
||||
|
@ -135,11 +123,6 @@ func NewGeocodeCache() *GeocodeCache {
|
|||
|
||||
// Get retrieves the results for a given key from the cache.
|
||||
func (rc *ResultsCache) Get(key CacheKey) ([]SearchResult, bool) {
|
||||
// Skip if RAM caching is disabled
|
||||
if !config.RamCacheEnabled {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
rc.mu.Lock()
|
||||
defer rc.mu.Unlock()
|
||||
|
||||
|
@ -160,11 +143,6 @@ func (rc *ResultsCache) Get(key CacheKey) ([]SearchResult, bool) {
|
|||
|
||||
// Set stores the results for a given key in the cache.
|
||||
func (rc *ResultsCache) Set(key CacheKey, results []SearchResult) {
|
||||
// Skip if RAM caching is disabled
|
||||
if !config.RamCacheEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
rc.mu.Lock()
|
||||
defer rc.mu.Unlock()
|
||||
|
||||
|
@ -184,11 +162,6 @@ func (rc *ResultsCache) keyToString(key CacheKey) string {
|
|||
|
||||
// checkAndCleanCache removes items if memory usage exceeds the limit.
|
||||
func (rc *ResultsCache) checkAndCleanCache() {
|
||||
// Skip if RAM caching is disabled
|
||||
if !config.RamCacheEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
if rc.currentMemoryUsage() > config.RamCache.MaxUsageBytes {
|
||||
rc.cleanOldestItems()
|
||||
}
|
||||
|
@ -206,11 +179,6 @@ func (rc *ResultsCache) currentMemoryUsage() uint64 {
|
|||
|
||||
// Get retrieves the geocoding result for a given query from the cache.
|
||||
func (gc *GeocodeCache) Get(query string) (latitude, longitude string, found bool, exists bool) {
|
||||
// Skip if RAM caching is disabled
|
||||
if !config.RamCacheEnabled {
|
||||
return "", "", false, false
|
||||
}
|
||||
|
||||
gc.mu.Lock()
|
||||
defer gc.mu.Unlock()
|
||||
|
||||
|
@ -230,11 +198,6 @@ func (gc *GeocodeCache) Get(query string) (latitude, longitude string, found boo
|
|||
}
|
||||
|
||||
func (gc *GeocodeCache) Set(query, latitude, longitude string, found bool) {
|
||||
// Skip if RAM caching is disabled
|
||||
if !config.RamCacheEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
gc.mu.Lock()
|
||||
defer gc.mu.Unlock()
|
||||
|
||||
|
@ -296,23 +259,15 @@ func convertToSearchResults(results interface{}) []SearchResult {
|
|||
genericResults[i] = r
|
||||
}
|
||||
return genericResults
|
||||
case []MusicResult:
|
||||
genericResults := make([]SearchResult, len(res))
|
||||
for i, r := range res {
|
||||
genericResults[i] = r
|
||||
}
|
||||
return genericResults
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertToSpecificResults(results []SearchResult) ([]TextSearchResult, []TorrentResult, []ImageSearchResult, []ForumSearchResult, []MusicResult) {
|
||||
func convertToSpecificResults(results []SearchResult) ([]TextSearchResult, []TorrentResult, []ImageSearchResult, []ForumSearchResult) {
|
||||
var textResults []TextSearchResult
|
||||
var torrentResults []TorrentResult
|
||||
var imageResults []ImageSearchResult
|
||||
var forumResults []ForumSearchResult
|
||||
var musicResults []MusicResult
|
||||
|
||||
for _, r := range results {
|
||||
switch res := r.(type) {
|
||||
case TextSearchResult:
|
||||
|
@ -323,9 +278,7 @@ func convertToSpecificResults(results []SearchResult) ([]TextSearchResult, []Tor
|
|||
imageResults = append(imageResults, res)
|
||||
case ForumSearchResult:
|
||||
forumResults = append(forumResults, res)
|
||||
case MusicResult:
|
||||
musicResults = append(musicResults, res)
|
||||
}
|
||||
}
|
||||
return textResults, torrentResults, imageResults, forumResults, musicResults
|
||||
return textResults, torrentResults, imageResults, forumResults
|
||||
}
|
||||
|
|
75
common.go
75
common.go
|
@ -8,7 +8,6 @@ import (
|
|||
"html/template"
|
||||
mathrand "math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
@ -37,12 +36,6 @@ type SearchEngine struct {
|
|||
Func func(string, string, string, int) ([]SearchResult, time.Duration, error)
|
||||
}
|
||||
|
||||
type LinkParts struct {
|
||||
Domain template.HTML
|
||||
Path template.HTML
|
||||
RootURL string // used by getFaviconProxyURL()
|
||||
}
|
||||
|
||||
// Helper function to render templates without elapsed time measurement
|
||||
func renderTemplate(w http.ResponseWriter, tmplName string, data map[string]interface{}) {
|
||||
// Generate icon paths for SVG and PNG, including a 1/10 chance for an alternate icon
|
||||
|
@ -114,71 +107,3 @@ func GetIconPath() (string, string) {
|
|||
// Default paths
|
||||
return "/static/images/icon.svg", "/static/images/icon.png"
|
||||
}
|
||||
|
||||
// FormatElapsedTime formats elapsed time as a string,
|
||||
// using:
|
||||
// - "> 0.01 ms" if under 49µs
|
||||
// - "0.xx ms" if under 1ms
|
||||
// - "xxx ms" if under 300ms
|
||||
// - "x.xx seconds" otherwise
|
||||
func FormatElapsedTime(elapsed time.Duration) string {
|
||||
if elapsed < 49*time.Microsecond {
|
||||
return fmt.Sprintf("> 0.01 %s", Translate("milliseconds"))
|
||||
} else if elapsed < time.Millisecond {
|
||||
ms := float64(elapsed.Microseconds()) / 1000.0
|
||||
return fmt.Sprintf("%.2f %s", ms, Translate("milliseconds"))
|
||||
} else if elapsed < 300*time.Millisecond {
|
||||
return fmt.Sprintf("%d %s", elapsed.Milliseconds(), Translate("milliseconds"))
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", elapsed.Seconds(), Translate("seconds"))
|
||||
}
|
||||
func FormatURLParts(rawURL string) (domain, path, rootURL string) {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil || parsed.Host == "" {
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
domain = parsed.Host
|
||||
if strings.HasPrefix(domain, "www.") {
|
||||
domain = domain[4:]
|
||||
}
|
||||
|
||||
rootURL = parsed.Scheme + "://" + parsed.Host
|
||||
|
||||
path = strings.Trim(parsed.Path, "/")
|
||||
pathSegments := strings.Split(path, "/")
|
||||
var cleanSegments []string
|
||||
for _, seg := range pathSegments {
|
||||
if seg != "" {
|
||||
cleanSegments = append(cleanSegments, seg)
|
||||
}
|
||||
}
|
||||
path = strings.Join(cleanSegments, "/")
|
||||
return domain, path, rootURL
|
||||
}
|
||||
|
||||
func FormatLinkHTML(rawURL string) LinkParts {
|
||||
domain, path, root := FormatURLParts(rawURL)
|
||||
|
||||
lp := LinkParts{
|
||||
RootURL: root,
|
||||
}
|
||||
|
||||
lp.Domain = template.HTML(fmt.Sprintf(`<span class="result-domain">%s</span>`, template.HTMLEscapeString(domain)))
|
||||
|
||||
if path != "" {
|
||||
pathDisplay := strings.ReplaceAll(path, "/", " › ")
|
||||
lp.Path = template.HTML(fmt.Sprintf(`<span class="result-path"> › %s</span>`, template.HTMLEscapeString(pathDisplay)))
|
||||
}
|
||||
|
||||
return lp
|
||||
}
|
||||
|
||||
// Converts any struct to a map[string]interface{} using JSON round-trip.
|
||||
// Useful for rendering templates with generic map input.
|
||||
func toMap(data interface{}) map[string]interface{} {
|
||||
jsonBytes, _ := json.Marshal(data)
|
||||
var result map[string]interface{}
|
||||
_ = json.Unmarshal(jsonBytes, &result)
|
||||
return result
|
||||
}
|
||||
|
|
162
config.go
162
config.go
|
@ -4,8 +4,10 @@ import (
|
|||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
|
@ -20,43 +22,24 @@ type CacheConfig struct {
|
|||
Path string
|
||||
}
|
||||
|
||||
type MetaSearchConfig struct {
|
||||
Text []string
|
||||
Image []string
|
||||
Files []string
|
||||
Video []string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Port int
|
||||
AuthCode string
|
||||
PeerID string
|
||||
Peers []string
|
||||
Domain string
|
||||
NodesEnabled bool
|
||||
MetaSearchEnabled bool
|
||||
IndexerEnabled bool
|
||||
WebsiteEnabled bool
|
||||
RamCacheEnabled bool
|
||||
DriveCacheEnabled bool
|
||||
MetaProxyEnabled bool
|
||||
MetaProxyStrict bool
|
||||
MetaProxyRetry int
|
||||
MetaProxies []string
|
||||
CrawlerProxyEnabled bool
|
||||
CrawlerProxyStrict bool
|
||||
CrawlerProxies []string
|
||||
CrawlerProxyRetry int
|
||||
// Maybye add Proxy support for Image Extraction?
|
||||
LogLevel int
|
||||
Port int // Added
|
||||
AuthCode string // Added
|
||||
PeerID string // Added
|
||||
Peers []string
|
||||
Domain string // Added
|
||||
NodesEnabled bool // Added
|
||||
CrawlerEnabled bool // Added
|
||||
IndexerEnabled bool // Added
|
||||
WebsiteEnabled bool // Added
|
||||
RamCacheEnabled bool
|
||||
DriveCacheEnabled bool // Added
|
||||
LogLevel int // Added
|
||||
ConcurrentStandardCrawlers int
|
||||
ConcurrentChromeCrawlers int
|
||||
CrawlingInterval time.Duration // Refres crawled results in...
|
||||
MaxPagesPerDomain int // Max pages to crawl per domain
|
||||
IndexBatchSize int
|
||||
LibreXInstances []string
|
||||
|
||||
MetaSearch MetaSearchConfig
|
||||
|
||||
DriveCache CacheConfig
|
||||
RamCache CacheConfig
|
||||
|
@ -68,52 +51,17 @@ var defaultConfig = Config{
|
|||
Peers: []string{},
|
||||
AuthCode: generateStrongRandomString(64),
|
||||
NodesEnabled: false,
|
||||
MetaSearchEnabled: true,
|
||||
CrawlerEnabled: true,
|
||||
IndexerEnabled: false,
|
||||
WebsiteEnabled: true,
|
||||
RamCacheEnabled: true,
|
||||
DriveCacheEnabled: false,
|
||||
MetaProxyEnabled: false,
|
||||
MetaProxyStrict: true,
|
||||
MetaProxies: []string{},
|
||||
MetaProxyRetry: 3,
|
||||
CrawlerProxyEnabled: false,
|
||||
CrawlerProxyStrict: true,
|
||||
CrawlerProxies: []string{},
|
||||
CrawlerProxyRetry: 1,
|
||||
ConcurrentStandardCrawlers: 12,
|
||||
ConcurrentChromeCrawlers: 4,
|
||||
CrawlingInterval: 24 * time.Hour,
|
||||
MaxPagesPerDomain: 10,
|
||||
IndexBatchSize: 50,
|
||||
LogLevel: 1,
|
||||
LibreXInstances: []string{"librex.antopie.org"},
|
||||
MetaSearch: MetaSearchConfig{
|
||||
// For Text search (skip SearXNG and LibreX by default, as that would be mega stupid)
|
||||
Text: []string{"Google", "Brave", "DuckDuckGo"},
|
||||
|
||||
// For Image search
|
||||
Image: []string{"Qwant", "Bing", "DeviantArt"},
|
||||
|
||||
// For Files search
|
||||
Files: []string{"TorrentGalaxy", "ThePirateBay"},
|
||||
|
||||
// For Video (piped instances)
|
||||
Video: []string{
|
||||
"api.piped.yt",
|
||||
"pipedapi.moomoo.me",
|
||||
"pipedapi.darkness.services",
|
||||
"pipedapi.kavin.rocks",
|
||||
"piped-api.hostux.net",
|
||||
"pipedapi.syncpundit.io",
|
||||
"piped-api.cfe.re",
|
||||
"pipedapi.in.projectsegfau.lt",
|
||||
"piapi.ggtyler.dev",
|
||||
"piped-api.codespace.cz",
|
||||
"pipedapi.coldforge.xyz",
|
||||
"pipedapi.osphost.fi",
|
||||
},
|
||||
},
|
||||
DriveCache: CacheConfig{
|
||||
Duration: 48 * time.Hour, // Added
|
||||
Path: "./cache", // Added
|
||||
|
@ -297,33 +245,14 @@ func saveConfig(config Config) {
|
|||
// Features section
|
||||
featuresSec := cfg.Section("Features")
|
||||
featuresSec.Key("Nodes").SetValue(strconv.FormatBool(config.NodesEnabled))
|
||||
featuresSec.Key("Crawler").SetValue(strconv.FormatBool(config.MetaSearchEnabled))
|
||||
featuresSec.Key("Crawler").SetValue(strconv.FormatBool(config.CrawlerEnabled))
|
||||
featuresSec.Key("Indexer").SetValue(strconv.FormatBool(config.IndexerEnabled))
|
||||
featuresSec.Key("Website").SetValue(strconv.FormatBool(config.WebsiteEnabled))
|
||||
featuresSec.Key("MetaProxy").SetValue(strconv.FormatBool(config.MetaProxyEnabled))
|
||||
featuresSec.Key("CrawlerProxy").SetValue(strconv.FormatBool(config.CrawlerProxyEnabled))
|
||||
|
||||
// Proxies section
|
||||
proxiesSec := cfg.Section("Proxies")
|
||||
proxiesSec.Key("MetaProxyStrict").SetValue(strconv.FormatBool(config.MetaProxyStrict))
|
||||
proxiesSec.Key("MetaProxies").SetValue(strings.Join(config.MetaProxies, ","))
|
||||
proxiesSec.Key("CrawlerProxyStrict").SetValue(strconv.FormatBool(config.CrawlerProxyStrict))
|
||||
proxiesSec.Key("CrawlerProxies").SetValue(strings.Join(config.CrawlerProxies, ","))
|
||||
proxiesSec.Key("MetaProxyRetry").SetValue(strconv.Itoa(config.MetaProxyRetry))
|
||||
proxiesSec.Key("CrawlerProxyRetry").SetValue(strconv.Itoa(config.CrawlerProxyRetry))
|
||||
|
||||
// MetaSearch section
|
||||
metaSec := cfg.Section("MetaSearches")
|
||||
metaSec.Key("LibreXInstances").SetValue(strings.Join(config.LibreXInstances, ","))
|
||||
metaSec.Key("Text").SetValue(strings.Join(config.MetaSearch.Text, ","))
|
||||
metaSec.Key("Image").SetValue(strings.Join(config.MetaSearch.Image, ","))
|
||||
metaSec.Key("Files").SetValue(strings.Join(config.MetaSearch.Files, ","))
|
||||
metaSec.Key("Video").SetValue(strings.Join(config.MetaSearch.Video, ","))
|
||||
|
||||
// Indexer section
|
||||
indexerSec := cfg.Section("Indexer")
|
||||
indexerSec.Key("ConcurrentStandardCrawlers").SetValue(strconv.Itoa(config.ConcurrentStandardCrawlers))
|
||||
indexerSec.Key("ConcurrentChromeCrawlers").SetValue(strconv.Itoa(config.ConcurrentChromeCrawlers))
|
||||
indexerSec.Key("ConcurrentChromeCrawlers").SetValue(strconv.Itoa(config.ConcurrentStandardCrawlers))
|
||||
indexerSec.Key("CrawlingInterval").SetValue(config.CrawlingInterval.String())
|
||||
indexerSec.Key("MaxPagesPerDomain").SetValue(strconv.Itoa(config.MaxPagesPerDomain))
|
||||
indexerSec.Key("IndexBatchSize").SetValue(strconv.Itoa(config.IndexBatchSize))
|
||||
|
@ -363,28 +292,11 @@ func loadConfig() Config {
|
|||
|
||||
// Features
|
||||
nodesEnabled := getConfigValueBool(cfg.Section("Features").Key("Nodes"), defaultConfig.NodesEnabled)
|
||||
metaSearchEnabled := getConfigValueBool(cfg.Section("Features").Key("Crawler"), defaultConfig.MetaSearchEnabled)
|
||||
crawlerEnabled := getConfigValueBool(cfg.Section("Features").Key("Crawler"), defaultConfig.CrawlerEnabled)
|
||||
indexerEnabled := getConfigValueBool(cfg.Section("Features").Key("Indexer"), defaultConfig.IndexerEnabled)
|
||||
websiteEnabled := getConfigValueBool(cfg.Section("Features").Key("Website"), defaultConfig.WebsiteEnabled)
|
||||
ramCacheEnabled := getConfigValueBool(cfg.Section("Features").Key("RamCache"), defaultConfig.RamCacheEnabled)
|
||||
driveCacheEnabled := getConfigValueBool(cfg.Section("Features").Key("DriveCache"), defaultConfig.DriveCacheEnabled)
|
||||
metaProxyEnabled := getConfigValueBool(cfg.Section("Features").Key("MetaProxy"), defaultConfig.MetaProxyEnabled)
|
||||
crawlerProxyEnabled := getConfigValueBool(cfg.Section("Features").Key("CrawlerProxy"), defaultConfig.CrawlerProxyEnabled)
|
||||
|
||||
// Proxies
|
||||
metaProxyStrict := getConfigValueBool(cfg.Section("Proxies").Key("MetaProxyStrict"), defaultConfig.MetaProxyStrict)
|
||||
metaProxies := strings.Split(getConfigValueString(cfg.Section("Proxies").Key("MetaProxies"), ""), ",")
|
||||
crawlerProxyStrict := getConfigValueBool(cfg.Section("Proxies").Key("CrawlerProxyStrict"), defaultConfig.CrawlerProxyStrict)
|
||||
crawlerProxies := strings.Split(getConfigValueString(cfg.Section("Proxies").Key("CrawlerProxies"), ""), ",")
|
||||
metaProxyRetry := getConfigValue(cfg.Section("Proxies").Key("MetaProxyRetry"), defaultConfig.MetaProxyRetry, strconv.Atoi)
|
||||
crawlerProxyRetry := getConfigValue(cfg.Section("Proxies").Key("CrawlerProxyRetry"), defaultConfig.CrawlerProxyRetry, strconv.Atoi)
|
||||
|
||||
// MetaSearch
|
||||
searchXInstances := strings.Split(getConfigValueString(cfg.Section("MetaSearches").Key("LibreXInstances"), strings.Join(defaultConfig.LibreXInstances, ",")), ",")
|
||||
textList := strings.Split(getConfigValueString(cfg.Section("MetaSearch").Key("Text"), strings.Join(defaultConfig.MetaSearch.Text, ",")), ",")
|
||||
imageList := strings.Split(getConfigValueString(cfg.Section("MetaSearch").Key("Image"), strings.Join(defaultConfig.MetaSearch.Image, ",")), ",")
|
||||
filesList := strings.Split(getConfigValueString(cfg.Section("MetaSearch").Key("Files"), strings.Join(defaultConfig.MetaSearch.Files, ",")), ",")
|
||||
videoList := strings.Split(getConfigValueString(cfg.Section("MetaSearch").Key("Video"), strings.Join(defaultConfig.MetaSearch.Video, ",")), ",")
|
||||
|
||||
// Indexing
|
||||
concurrentStandardCrawlers := getConfigValue(cfg.Section("Indexer").Key("ConcurrentStandardCrawlers"), defaultConfig.ConcurrentStandardCrawlers, strconv.Atoi)
|
||||
|
@ -413,31 +325,16 @@ func loadConfig() Config {
|
|||
AuthCode: authCode,
|
||||
Peers: peers,
|
||||
NodesEnabled: nodesEnabled,
|
||||
MetaSearchEnabled: metaSearchEnabled,
|
||||
CrawlerEnabled: crawlerEnabled,
|
||||
IndexerEnabled: indexerEnabled,
|
||||
WebsiteEnabled: websiteEnabled,
|
||||
RamCacheEnabled: ramCacheEnabled,
|
||||
DriveCacheEnabled: driveCacheEnabled,
|
||||
MetaProxyEnabled: metaProxyEnabled,
|
||||
MetaProxyStrict: metaProxyStrict,
|
||||
MetaProxies: metaProxies,
|
||||
MetaProxyRetry: metaProxyRetry,
|
||||
CrawlerProxyEnabled: crawlerProxyEnabled,
|
||||
CrawlerProxyStrict: crawlerProxyStrict,
|
||||
CrawlerProxies: crawlerProxies,
|
||||
CrawlerProxyRetry: crawlerProxyRetry,
|
||||
ConcurrentStandardCrawlers: concurrentStandardCrawlers,
|
||||
ConcurrentChromeCrawlers: concurrentChromeCrawlers,
|
||||
CrawlingInterval: crawlingInterval,
|
||||
MaxPagesPerDomain: maxPagesPerDomain,
|
||||
IndexBatchSize: indexBatchSize,
|
||||
LibreXInstances: searchXInstances,
|
||||
MetaSearch: MetaSearchConfig{
|
||||
Text: textList,
|
||||
Image: imageList,
|
||||
Files: filesList,
|
||||
Video: videoList,
|
||||
},
|
||||
DriveCache: CacheConfig{
|
||||
Duration: driveDuration,
|
||||
MaxUsageBytes: driveMaxUsage,
|
||||
|
@ -535,6 +432,27 @@ func parseMaxUsageDrive(value string, cachePath string) uint64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
// Get total disk space of the system where cachePath resides
|
||||
func getTotalDiskSpace(cachePath string) uint64 {
|
||||
var stat syscall.Statfs_t
|
||||
|
||||
// Get filesystem stats for the cache path
|
||||
absPath, err := filepath.Abs(cachePath)
|
||||
if err != nil {
|
||||
printErr("Failed to resolve absolute path for: %s", cachePath)
|
||||
return 0
|
||||
}
|
||||
|
||||
err = syscall.Statfs(absPath, &stat)
|
||||
if err != nil {
|
||||
printErr("Failed to retrieve filesystem stats for: %s", absPath)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Total disk space in bytes
|
||||
return stat.Blocks * uint64(stat.Bsize)
|
||||
}
|
||||
|
||||
// Helper to format bytes back to human-readable string
|
||||
func formatMaxUsage(bytes uint64) string {
|
||||
const GiB = 1024 * 1024 * 1024
|
||||
|
|
|
@ -32,12 +32,8 @@ func fetchPageMetadataStandard(pageURL, userAgent string) (string, string, strin
|
|||
|
||||
// fetchPageMetadataChrome uses Chromedp to handle JavaScript-rendered pages.
|
||||
func fetchPageMetadataChrome(pageURL, userAgent string) (string, string, string) {
|
||||
// Create a custom allocator context for Chromedp with proxy support if enabled
|
||||
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), configureChromeOptions()...)
|
||||
defer cancelAlloc()
|
||||
|
||||
// Create a browser context
|
||||
ctx, cancel := chromedp.NewContext(allocCtx)
|
||||
// Create context
|
||||
ctx, cancel := chromedp.NewContext(context.Background())
|
||||
defer cancel()
|
||||
|
||||
var renderedHTML string
|
||||
|
@ -61,36 +57,9 @@ func fetchPageMetadataChrome(pageURL, userAgent string) (string, string, string)
|
|||
return extractParsedDOM(doc)
|
||||
}
|
||||
|
||||
// configureChromeOptions sets up Chrome options and proxy if CrawlerProxy is enabled.
|
||||
func configureChromeOptions() []chromedp.ExecAllocatorOption {
|
||||
options := chromedp.DefaultExecAllocatorOptions[:]
|
||||
|
||||
// This code is not using config.CrawlerProxyRetry
|
||||
if config.CrawlerProxyEnabled && crawlerProxyClient != nil {
|
||||
// Retrieve proxy settings from CrawlerProxy
|
||||
proxy := crawlerProxyClient.GetProxy() // Ensure a `GetProxy` method is implemented for your proxy client
|
||||
if proxy != "" {
|
||||
options = append(options, chromedp.ProxyServer(proxy))
|
||||
printDebug("Using CrawlerProxy for Chromedp: %s", proxy)
|
||||
} else {
|
||||
printWarn("CrawlerProxy is enabled but no valid proxy is available")
|
||||
}
|
||||
}
|
||||
|
||||
// // Add additional Chrome
|
||||
// options = append(options,
|
||||
// chromedp.Flag("headless", true),
|
||||
// chromedp.Flag("disable-gpu", true),
|
||||
// chromedp.Flag("no-sandbox", true),
|
||||
// chromedp.Flag("disable-setuid-sandbox", true),
|
||||
// )
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// extractStandard does the normal HTML parse with OG, Twitter, etc.
|
||||
func extractStandard(pageURL, userAgent string) (title, desc, keywords string) {
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
req, err := http.NewRequest("GET", pageURL, nil)
|
||||
if err != nil {
|
||||
printDebug("Failed to create request for %s: %v", pageURL, err)
|
||||
|
@ -99,8 +68,7 @@ func extractStandard(pageURL, userAgent string) (title, desc, keywords string) {
|
|||
req.Header.Set("User-Agent", userAgent)
|
||||
req.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
|
||||
// Use CrawlerProxy if enabled
|
||||
resp, err := DoCrawlerProxyRequest(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
printDebug("Failed to GET %s: %v", pageURL, err)
|
||||
return
|
||||
|
@ -208,6 +176,7 @@ func fallbackReadability(pageURL, userAgent, title, desc, keywords string) (stri
|
|||
return title, desc, keywords
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
readReq, err := http.NewRequest("GET", pageURL, nil)
|
||||
if err != nil {
|
||||
printDebug("Failed to create fallbackReadability request: %v", err)
|
||||
|
@ -216,16 +185,14 @@ func fallbackReadability(pageURL, userAgent, title, desc, keywords string) (stri
|
|||
readReq.Header.Set("User-Agent", userAgent)
|
||||
readReq.Header.Set("Accept-Language", "en-US,en;q=0.9")
|
||||
|
||||
// Use CrawlerProxy if enabled
|
||||
readResp, err := DoCrawlerProxyRequest(readReq)
|
||||
if err != nil {
|
||||
printDebug("go-readability GET error for %s: %v", pageURL, err)
|
||||
return title, desc, keywords
|
||||
}
|
||||
|
||||
if readResp.StatusCode < 200 || readResp.StatusCode >= 300 {
|
||||
printDebug("go-readability GET returned status %d for %s", readResp.StatusCode, pageURL)
|
||||
readResp.Body.Close() // Safely close body
|
||||
readResp, err := client.Do(readReq)
|
||||
if err != nil || readResp.StatusCode < 200 || readResp.StatusCode >= 300 {
|
||||
if err != nil {
|
||||
printDebug("go-readability GET error for %s: %v", pageURL, err)
|
||||
}
|
||||
if readResp != nil {
|
||||
readResp.Body.Close()
|
||||
}
|
||||
return title, desc, keywords
|
||||
}
|
||||
defer readResp.Body.Close()
|
||||
|
|
27
disk.go
27
disk.go
|
@ -1,27 +0,0 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func getTotalDiskSpace(cachePath string) uint64 {
|
||||
var stat syscall.Statfs_t
|
||||
|
||||
absPath, err := filepath.Abs(cachePath)
|
||||
if err != nil {
|
||||
printErr("Failed to resolve absolute path for: %s", cachePath)
|
||||
return 0
|
||||
}
|
||||
|
||||
err = syscall.Statfs(absPath, &stat)
|
||||
if err != nil {
|
||||
printErr("Failed to retrieve filesystem stats for: %s", absPath)
|
||||
return 0
|
||||
}
|
||||
|
||||
return stat.Blocks * uint64(stat.Bsize)
|
||||
}
|
36
disk_win.go
36
disk_win.go
|
@ -1,36 +0,0 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func getTotalDiskSpace(path string) uint64 {
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
getDiskFreeSpaceExW := kernel32.NewProc("GetDiskFreeSpaceExW")
|
||||
|
||||
lpDirectoryName, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
printErr("Failed to encode path for Windows API: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
var freeBytesAvailable, totalNumberOfBytes, totalNumberOfFreeBytes uint64
|
||||
|
||||
r1, _, err := getDiskFreeSpaceExW.Call(
|
||||
uintptr(unsafe.Pointer(lpDirectoryName)),
|
||||
uintptr(unsafe.Pointer(&freeBytesAvailable)),
|
||||
uintptr(unsafe.Pointer(&totalNumberOfBytes)),
|
||||
uintptr(unsafe.Pointer(&totalNumberOfFreeBytes)),
|
||||
)
|
||||
|
||||
if r1 == 0 {
|
||||
printErr("GetDiskFreeSpaceExW failed: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
return totalNumberOfBytes
|
||||
}
|
574
favicon.go
574
favicon.go
|
@ -1,574 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/fyne-io/image/ico"
|
||||
"golang.org/x/image/bmp"
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/tiff"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
var (
|
||||
faviconCache = struct {
|
||||
sync.RWMutex
|
||||
m map[string]bool // tracks in-progress downloads
|
||||
}{m: make(map[string]bool)}
|
||||
|
||||
// Common favicon paths to try
|
||||
commonFaviconPaths = []string{
|
||||
"/favicon.ico",
|
||||
"/favicon.png",
|
||||
"/favicon.jpg",
|
||||
"/favicon.jpeg",
|
||||
"/favicon.webp",
|
||||
"/apple-touch-icon.png",
|
||||
"/apple-touch-icon-precomposed.png",
|
||||
}
|
||||
|
||||
// Regex to extract favicon URLs from HTML
|
||||
iconLinkRegex = regexp.MustCompile(`<link[^>]+rel=["'](?:icon|shortcut icon|apple-touch-icon)["'][^>]+href=["']([^"']+)["']`)
|
||||
)
|
||||
|
||||
// Add this near the top with other vars
|
||||
var (
|
||||
faviconDownloadQueue = make(chan faviconDownloadRequest, 1000)
|
||||
)
|
||||
|
||||
type faviconDownloadRequest struct {
|
||||
faviconURL string
|
||||
pageURL string
|
||||
cacheID string
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Start 5 worker goroutines to process favicon downloads
|
||||
for i := 0; i < 5; i++ {
|
||||
go faviconDownloadWorker()
|
||||
}
|
||||
}
|
||||
|
||||
func faviconDownloadWorker() {
|
||||
for req := range faviconDownloadQueue {
|
||||
cacheFavicon(req.faviconURL, req.cacheID)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates a cache ID from URL
|
||||
func faviconIDFromURL(rawURL string) string {
|
||||
hasher := md5.New()
|
||||
hasher.Write([]byte(rawURL))
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
|
||||
// Resolves favicon URL using multiple methods
|
||||
func resolveFaviconURL(rawFavicon, pageURL string) (faviconURL, cacheID string) {
|
||||
cacheID = faviconIDFromURL(pageURL)
|
||||
|
||||
// Handle data URLs first
|
||||
if strings.HasPrefix(rawFavicon, "data:image") {
|
||||
parts := strings.SplitN(rawFavicon, ";base64,", 2)
|
||||
if len(parts) == 2 {
|
||||
data, err := base64.StdEncoding.DecodeString(parts[1])
|
||||
if err == nil {
|
||||
hasher := md5.New()
|
||||
hasher.Write(data)
|
||||
return rawFavicon, hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
}
|
||||
return "", "" // Invalid data URL
|
||||
}
|
||||
|
||||
// Existing URL handling logic
|
||||
if rawFavicon != "" && strings.HasPrefix(rawFavicon, "http") {
|
||||
cacheID = faviconIDFromURL(rawFavicon)
|
||||
return rawFavicon, cacheID
|
||||
}
|
||||
|
||||
parsedPage, err := url.Parse(pageURL)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// Method 1: Parse HTML
|
||||
if favicon := findFaviconInHTML(pageURL); favicon != "" {
|
||||
if strings.HasPrefix(favicon, "http") {
|
||||
return favicon, faviconIDFromURL(favicon)
|
||||
}
|
||||
resolved := resolveRelativeURL(parsedPage, favicon)
|
||||
return resolved, faviconIDFromURL(resolved)
|
||||
}
|
||||
|
||||
// Method 2: Common paths
|
||||
for _, path := range commonFaviconPaths {
|
||||
testURL := "https://" + parsedPage.Host + path
|
||||
if checkURLExists(testURL) {
|
||||
return testURL, faviconIDFromURL(testURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Method 3: HTTP headers
|
||||
if headerIcon := findFaviconInHeaders(pageURL); headerIcon != "" {
|
||||
if strings.HasPrefix(headerIcon, "http") {
|
||||
return headerIcon, faviconIDFromURL(headerIcon)
|
||||
}
|
||||
resolved := resolveRelativeURL(parsedPage, headerIcon)
|
||||
return resolved, faviconIDFromURL(resolved)
|
||||
}
|
||||
|
||||
// Fallback
|
||||
fallbackURL := "https://" + parsedPage.Host + "/favicon.ico"
|
||||
return fallbackURL, faviconIDFromURL(fallbackURL)
|
||||
}
|
||||
|
||||
// Checks HTTP headers for favicon links
|
||||
func findFaviconInHeaders(pageURL string) string {
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second, // like 3 seconds for favicon should be enough
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("HEAD", pageURL, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Add User-Agent
|
||||
userAgent, err := GetUserAgent("findFaviconInHeaders")
|
||||
if err != nil {
|
||||
printWarn("Error getting User-Agent: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check Link headers (common for favicons)
|
||||
if links, ok := resp.Header["Link"]; ok {
|
||||
for _, link := range links {
|
||||
parts := strings.Split(link, ";")
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
urlPart := strings.TrimSpace(parts[0])
|
||||
if !strings.HasPrefix(urlPart, "<") || !strings.HasSuffix(urlPart, ">") {
|
||||
continue
|
||||
}
|
||||
|
||||
urlPart = urlPart[1 : len(urlPart)-1] // Remove < and >
|
||||
for _, part := range parts[1:] {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.EqualFold(part, `rel="icon"`) ||
|
||||
strings.EqualFold(part, `rel=icon`) ||
|
||||
strings.EqualFold(part, `rel="shortcut icon"`) ||
|
||||
strings.EqualFold(part, `rel=shortcut icon`) {
|
||||
return urlPart
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Helper to resolve relative URLs
|
||||
func resolveRelativeURL(base *url.URL, relative string) string {
|
||||
if strings.HasPrefix(relative, "http") {
|
||||
return relative
|
||||
}
|
||||
if strings.HasPrefix(relative, "//") {
|
||||
return base.Scheme + ":" + relative
|
||||
}
|
||||
if strings.HasPrefix(relative, "/") {
|
||||
return base.Scheme + "://" + base.Host + relative
|
||||
}
|
||||
return base.Scheme + "://" + base.Host + base.Path + "/" + relative
|
||||
}
|
||||
|
||||
// Checks if a URL exists (returns 200 OK)
|
||||
func checkURLExists(url string) bool {
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add User-Agent
|
||||
userAgent, err := GetUserAgent("Text-Search-Brave")
|
||||
if err != nil {
|
||||
printWarn("Error getting User-Agent: %v", err)
|
||||
}
|
||||
req.Header.Set("checkURLExists", userAgent)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
|
||||
// Fetches HTML and looks for favicon links
|
||||
func findFaviconInHTML(pageURL string) string {
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", pageURL, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Add User-Agent
|
||||
userAgent, err := GetUserAgent("findFaviconInHTML")
|
||||
if err != nil {
|
||||
printWarn("Error getting User-Agent: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check if this is an AMP page
|
||||
isAMP := false
|
||||
for _, attr := range resp.Header["Link"] {
|
||||
if strings.Contains(attr, "rel=\"amphtml\"") {
|
||||
isAMP = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Parse HTML
|
||||
doc, err := html.Parse(resp.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var faviconURL string
|
||||
var findLinks func(*html.Node)
|
||||
findLinks = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "link" {
|
||||
var rel, href string
|
||||
for _, attr := range n.Attr {
|
||||
switch attr.Key {
|
||||
case "rel":
|
||||
rel = attr.Val
|
||||
case "href":
|
||||
href = attr.Val
|
||||
}
|
||||
}
|
||||
|
||||
// Prioritize different favicon types
|
||||
if href != "" {
|
||||
switch rel {
|
||||
case "icon", "shortcut icon", "apple-touch-icon", "apple-touch-icon-precomposed":
|
||||
// For AMP pages, prefer the non-versioned URL if possible
|
||||
if isAMP {
|
||||
if u, err := url.Parse(href); err == nil {
|
||||
u.RawQuery = "" // Remove query parameters
|
||||
href = u.String()
|
||||
}
|
||||
}
|
||||
if faviconURL == "" || // First found
|
||||
rel == "apple-touch-icon" || // Prefer apple-touch-icon
|
||||
rel == "icon" { // Then regular icon
|
||||
faviconURL = href
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
||||
findLinks(c)
|
||||
}
|
||||
}
|
||||
findLinks(doc)
|
||||
|
||||
return faviconURL
|
||||
}
|
||||
|
||||
func getFaviconProxyURL(rawFavicon, pageURL string) string {
|
||||
if pageURL == "" {
|
||||
return "/static/images/globe.svg"
|
||||
}
|
||||
|
||||
cacheID := faviconIDFromURL(pageURL)
|
||||
filename := fmt.Sprintf("%s_icon.webp", cacheID)
|
||||
cachedPath := filepath.Join(config.DriveCache.Path, "images", filename)
|
||||
|
||||
if _, err := os.Stat(cachedPath); err == nil {
|
||||
return fmt.Sprintf("/image/%s_icon.webp", cacheID)
|
||||
}
|
||||
|
||||
// Resolve URL
|
||||
faviconURL, _ := resolveFaviconURL(rawFavicon, pageURL)
|
||||
if faviconURL == "" {
|
||||
recordInvalidImageID(cacheID)
|
||||
return "/static/images/globe.svg"
|
||||
}
|
||||
|
||||
// Check if already downloading
|
||||
faviconCache.RLock()
|
||||
downloading := faviconCache.m[cacheID]
|
||||
faviconCache.RUnlock()
|
||||
|
||||
if !downloading {
|
||||
faviconCache.Lock()
|
||||
faviconCache.m[cacheID] = true
|
||||
faviconCache.Unlock()
|
||||
|
||||
// Send to download queue instead of starting goroutine
|
||||
faviconDownloadQueue <- faviconDownloadRequest{
|
||||
faviconURL: faviconURL,
|
||||
pageURL: pageURL,
|
||||
cacheID: cacheID,
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/image/%s_icon.webp", cacheID)
|
||||
}
|
||||
|
||||
// Caches favicon, always saving *_icon.webp
|
||||
func cacheFavicon(imageURL, imageID string) (string, bool, error) {
|
||||
// if imageURL == "" {
|
||||
// recordInvalidImageID(imageID)
|
||||
// return "", false, fmt.Errorf("empty image URL for image ID %s", imageID)
|
||||
// }
|
||||
|
||||
// Debug
|
||||
fmt.Printf("Downloading favicon [%s] for ID [%s]\n", imageURL, imageID)
|
||||
|
||||
filename := fmt.Sprintf("%s_icon.webp", imageID)
|
||||
imageCacheDir := filepath.Join(config.DriveCache.Path, "images")
|
||||
if err := os.MkdirAll(imageCacheDir, 0755); err != nil {
|
||||
return "", false, fmt.Errorf("couldn't create images folder: %v", err)
|
||||
}
|
||||
cachedImagePath := filepath.Join(imageCacheDir, filename)
|
||||
tempImagePath := cachedImagePath + ".tmp"
|
||||
|
||||
// Already cached?
|
||||
if _, err := os.Stat(cachedImagePath); err == nil {
|
||||
return cachedImagePath, true, nil
|
||||
}
|
||||
|
||||
cachingImagesMu.Lock()
|
||||
if _, exists := cachingImages[imageURL]; !exists {
|
||||
cachingImages[imageURL] = &sync.Mutex{}
|
||||
}
|
||||
mu := cachingImages[imageURL]
|
||||
cachingImagesMu.Unlock()
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Recheck after lock
|
||||
if _, err := os.Stat(cachedImagePath); err == nil {
|
||||
return cachedImagePath, true, nil
|
||||
}
|
||||
|
||||
cachingSemaphore <- struct{}{}
|
||||
defer func() { <-cachingSemaphore }()
|
||||
|
||||
var data []byte
|
||||
var contentType string
|
||||
|
||||
// Handle data URLs
|
||||
if strings.HasPrefix(imageURL, "data:") {
|
||||
commaIndex := strings.Index(imageURL, ",")
|
||||
if commaIndex == -1 {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, fmt.Errorf("invalid data URL: no comma")
|
||||
}
|
||||
headerPart := imageURL[:commaIndex]
|
||||
dataPart := imageURL[commaIndex+1:]
|
||||
|
||||
mediaType := "text/plain"
|
||||
base64Encoded := false
|
||||
if strings.HasPrefix(headerPart, "data:") {
|
||||
mediaTypePart := headerPart[5:]
|
||||
mediaTypeParts := strings.SplitN(mediaTypePart, ";", 2)
|
||||
mediaType = mediaTypeParts[0]
|
||||
if len(mediaTypeParts) > 1 {
|
||||
for _, param := range strings.Split(mediaTypeParts[1], ";") {
|
||||
param = strings.TrimSpace(param)
|
||||
if param == "base64" {
|
||||
base64Encoded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if base64Encoded {
|
||||
data, _ = base64.StdEncoding.DecodeString(dataPart)
|
||||
} else {
|
||||
decodedStr, err := url.QueryUnescape(dataPart)
|
||||
if err != nil {
|
||||
data = []byte(dataPart)
|
||||
} else {
|
||||
data = []byte(decodedStr)
|
||||
}
|
||||
}
|
||||
|
||||
contentType = mediaType
|
||||
} else {
|
||||
// Download from HTTP URL
|
||||
client := &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", imageURL, nil)
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// Add User-Agent
|
||||
userAgent, err := GetUserAgent("Text-Search-Brave")
|
||||
if err != nil {
|
||||
printWarn("Error getting User-Agent: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
contentType = http.DetectContentType(data)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, fmt.Errorf("URL did not return an image: %s", imageURL)
|
||||
}
|
||||
|
||||
// SVG special case
|
||||
if contentType == "image/svg+xml" {
|
||||
err := os.WriteFile(tempImagePath, data, 0644)
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
err = os.Rename(tempImagePath, cachedImagePath)
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
cachingImagesMu.Lock()
|
||||
delete(cachingImages, imageURL)
|
||||
cachingImagesMu.Unlock()
|
||||
return cachedImagePath, true, nil
|
||||
}
|
||||
|
||||
// Decode image
|
||||
var img image.Image
|
||||
var err error
|
||||
switch contentType {
|
||||
case "image/x-icon", "image/vnd.microsoft.icon":
|
||||
img, err = ico.Decode(bytes.NewReader(data))
|
||||
case "image/jpeg":
|
||||
img, err = jpeg.Decode(bytes.NewReader(data))
|
||||
case "image/png":
|
||||
img, err = png.Decode(bytes.NewReader(data))
|
||||
case "image/gif":
|
||||
img, err = gif.Decode(bytes.NewReader(data))
|
||||
case "image/webp":
|
||||
img, err = webp.Decode(bytes.NewReader(data))
|
||||
case "image/bmp":
|
||||
img, err = bmp.Decode(bytes.NewReader(data))
|
||||
case "image/tiff":
|
||||
img, err = tiff.Decode(bytes.NewReader(data))
|
||||
default:
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, fmt.Errorf("unsupported image type: %s", contentType)
|
||||
}
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
// Resize
|
||||
maxSize := 16
|
||||
width := img.Bounds().Dx()
|
||||
height := img.Bounds().Dy()
|
||||
|
||||
if width > maxSize || height > maxSize {
|
||||
dst := image.NewRGBA(image.Rect(0, 0, maxSize, maxSize))
|
||||
draw.ApproxBiLinear.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil)
|
||||
img = dst
|
||||
}
|
||||
|
||||
// Save as WebP
|
||||
outFile, err := os.Create(tempImagePath)
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
options := &webp.Options{Lossless: false, Quality: 80}
|
||||
err = webp.Encode(outFile, img, options)
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
err = os.Rename(tempImagePath, cachedImagePath)
|
||||
if err != nil {
|
||||
recordInvalidImageID(imageID)
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
cachingImagesMu.Lock()
|
||||
delete(cachingImages, imageURL)
|
||||
cachingImagesMu.Unlock()
|
||||
|
||||
return cachedImagePath, true, nil
|
||||
}
|
|
@ -57,34 +57,31 @@ func (t *ThePirateBay) Search(query string, category string) ([]TorrentResult, e
|
|||
return []TorrentResult{}, nil
|
||||
}
|
||||
|
||||
searchURL := fmt.Sprintf("https://%s/q.php?q=%s&cat=%s", PIRATEBAY_DOMAIN, url.QueryEscape(query), categoryCode)
|
||||
url := fmt.Sprintf("https://%s/q.php?q=%s&cat=%s", PIRATEBAY_DOMAIN, url.QueryEscape(query), categoryCode)
|
||||
|
||||
// User Agent generation
|
||||
userAgent, err := GetUserAgent("files-tpb")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating User-Agent: %w", err)
|
||||
fmt.Println("Error:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
// Perform the request using MetaProxy if enabled
|
||||
resp, err := DoMetaProxyRequest(req)
|
||||
client := &http.Client{}
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request to The Pirate Bay: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
var torrentData []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&torrentData); err != nil {
|
||||
return nil, fmt.Errorf("error decoding response JSON: %w", err)
|
||||
if err := json.NewDecoder(response.Body).Decode(&torrentData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []TorrentResult
|
||||
|
|
|
@ -62,17 +62,18 @@ func (tg *TorrentGalaxy) Search(query string, category string) ([]TorrentResult,
|
|||
// User Agent generation
|
||||
userAgent, err := GetUserAgent("files-torrentgalaxy")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error generating User-Agent: %w", err)
|
||||
fmt.Println("Error:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating request: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
// Perform the request using MetaProxy if enabled
|
||||
resp, err := DoMetaProxyRequest(req)
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error making request to TorrentGalaxy: %w", err)
|
||||
}
|
||||
|
|
43
files.go
43
files.go
|
@ -30,25 +30,11 @@ var (
|
|||
|
||||
var fileResultsChan = make(chan []TorrentResult)
|
||||
|
||||
func initFileEngines() {
|
||||
|
||||
torrentGalaxy = nil
|
||||
thePirateBay = nil
|
||||
// nyaa = nil
|
||||
// rutor = nil
|
||||
|
||||
for _, engineName := range config.MetaSearch.Files {
|
||||
switch engineName {
|
||||
case "TorrentGalaxy":
|
||||
torrentGalaxy = NewTorrentGalaxy()
|
||||
case "ThePirateBay":
|
||||
thePirateBay = NewThePirateBay()
|
||||
// case "Nyaa":
|
||||
// nyaa = NewNyaa()
|
||||
// case "Rutor":
|
||||
// rutor = NewRutor()
|
||||
}
|
||||
}
|
||||
func init() {
|
||||
torrentGalaxy = NewTorrentGalaxy()
|
||||
// nyaa = NewNyaa()
|
||||
thePirateBay = NewThePirateBay()
|
||||
// rutor = NewRutor()
|
||||
}
|
||||
|
||||
func handleFileSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
||||
|
@ -66,7 +52,7 @@ func handleFileSearch(w http.ResponseWriter, settings UserSettings, query string
|
|||
data := map[string]interface{}{
|
||||
"Results": combinedResults,
|
||||
"Query": query,
|
||||
"Fetched": FormatElapsedTime(elapsedTime),
|
||||
"Fetched": fmt.Sprintf("%.2f %s", elapsedTime.Seconds(), Translate("seconds")), // Time for fetching results
|
||||
"Category": "all",
|
||||
"Sort": "seed",
|
||||
"Page": page,
|
||||
|
@ -102,7 +88,7 @@ func getFileResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string,
|
|||
case results := <-cacheChan:
|
||||
if results == nil {
|
||||
// Fetch only if the cache miss occurs and Crawler is enabled
|
||||
if config.MetaSearchEnabled {
|
||||
if config.CrawlerEnabled {
|
||||
combinedResults = fetchFileResults(query, safe, lang, page)
|
||||
if len(combinedResults) > 0 {
|
||||
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
|
||||
|
@ -111,12 +97,12 @@ func getFileResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string,
|
|||
printDebug("Crawler disabled; skipping fetching.")
|
||||
}
|
||||
} else {
|
||||
_, torrentResults, _, _, _ := convertToSpecificResults(results)
|
||||
_, torrentResults, _, _ := convertToSpecificResults(results)
|
||||
combinedResults = torrentResults
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
printDebug("Cache check timeout")
|
||||
if config.MetaSearchEnabled {
|
||||
if config.CrawlerEnabled {
|
||||
combinedResults = fetchFileResults(query, safe, lang, page)
|
||||
if len(combinedResults) > 0 {
|
||||
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
|
||||
|
@ -131,13 +117,13 @@ func getFileResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string,
|
|||
|
||||
func fetchFileResults(query, safe, lang string, page int) []TorrentResult {
|
||||
// If Crawler is disabled, skip fetching from torrent sites
|
||||
if !config.MetaSearchEnabled {
|
||||
if !config.CrawlerEnabled {
|
||||
printInfo("Crawler is disabled; skipping torrent site fetching.")
|
||||
return []TorrentResult{}
|
||||
}
|
||||
|
||||
sites := []TorrentSite{torrentGalaxy, nyaa, thePirateBay, rutor}
|
||||
var results []TorrentResult
|
||||
results := []TorrentResult{}
|
||||
|
||||
for _, site := range sites {
|
||||
if site == nil {
|
||||
|
@ -154,12 +140,9 @@ func fetchFileResults(query, safe, lang string, page int) []TorrentResult {
|
|||
}
|
||||
}
|
||||
|
||||
// If no results, try from other nodes
|
||||
if len(results) == 0 {
|
||||
if config.NodesEnabled {
|
||||
printWarn("No file results found for query: %s, trying other nodes", query)
|
||||
results = tryOtherNodesForFileSearch(query, safe, lang, page, []string{hostID})
|
||||
}
|
||||
printWarn("No file results found for query: %s, trying other nodes", query)
|
||||
results = tryOtherNodesForFileSearch(query, safe, lang, page, []string{hostID})
|
||||
}
|
||||
|
||||
return results
|
||||
|
|
58
forums.go
58
forums.go
|
@ -3,57 +3,54 @@ package main
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func PerformRedditSearch(query string, safe string, page int) ([]ForumSearchResult, error) {
|
||||
if !config.MetaSearchEnabled {
|
||||
if !config.CrawlerEnabled {
|
||||
printDebug("Crawler is disabled; skipping forum search.")
|
||||
return []ForumSearchResult{}, nil
|
||||
}
|
||||
|
||||
const (
|
||||
pageSize = 25
|
||||
baseURL = "https://www.reddit.com"
|
||||
pageSize = 25
|
||||
baseURL = "https://www.reddit.com"
|
||||
maxRetries = 5
|
||||
initialBackoff = 2 * time.Second
|
||||
)
|
||||
|
||||
var results []ForumSearchResult
|
||||
offset := page * pageSize
|
||||
searchURL := fmt.Sprintf("%s/search.json?q=%s&limit=%d&start=%d",
|
||||
baseURL,
|
||||
url.QueryEscape(query),
|
||||
pageSize,
|
||||
offset,
|
||||
)
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %v", err)
|
||||
searchURL := fmt.Sprintf("%s/search.json?q=%s&limit=%d&start=%d", baseURL, url.QueryEscape(query), pageSize, page*pageSize)
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
||||
// Retry logic with exponential backoff
|
||||
for i := 0; i <= maxRetries; i++ {
|
||||
resp, err = http.Get(searchURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusTooManyRequests {
|
||||
break
|
||||
}
|
||||
|
||||
// Wait for some time before retrying
|
||||
backoff := time.Duration(math.Pow(2, float64(i))) * initialBackoff
|
||||
time.Sleep(backoff)
|
||||
}
|
||||
|
||||
// Set User-Agent
|
||||
userAgent, uaErr := GetUserAgent("Reddit-Forum-Search")
|
||||
if uaErr != nil {
|
||||
return nil, fmt.Errorf("getting user agent: %v", uaErr)
|
||||
}
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
// Make request using MetaProxy logic
|
||||
resp, err := DoMetaProxyRequest(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Validate response status
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
var searchResults map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&searchResults); err != nil {
|
||||
return nil, fmt.Errorf("decoding response: %v", err)
|
||||
|
@ -69,9 +66,9 @@ func PerformRedditSearch(query string, safe string, page int) ([]ForumSearchResu
|
|||
return nil, fmt.Errorf("no children field in data")
|
||||
}
|
||||
|
||||
// Extract search results
|
||||
for _, post := range posts {
|
||||
postData := post.(map[string]interface{})["data"].(map[string]interface{})
|
||||
|
||||
if safe == "active" && postData["over_18"].(bool) {
|
||||
continue
|
||||
}
|
||||
|
@ -81,7 +78,6 @@ func PerformRedditSearch(query string, safe string, page int) ([]ForumSearchResu
|
|||
if len(description) > 500 {
|
||||
description = description[:500] + "..."
|
||||
}
|
||||
|
||||
publishedDate := time.Unix(int64(postData["created_utc"].(float64)), 0)
|
||||
permalink := postData["permalink"].(string)
|
||||
resultURL := fmt.Sprintf("%s%s", baseURL, permalink)
|
||||
|
@ -120,7 +116,7 @@ func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query stri
|
|||
"Query": query,
|
||||
"Results": results,
|
||||
"Page": page,
|
||||
"Fetched": FormatElapsedTime(elapsedTime),
|
||||
"Fetched": fmt.Sprintf("%.2f %s", elapsedTime.Seconds(), Translate("seconds")), // Time for fetching results
|
||||
"HasPrevPage": page > 1,
|
||||
"HasNextPage": len(results) >= 25,
|
||||
"NoResults": len(results) == 0,
|
||||
|
@ -154,7 +150,7 @@ func getForumResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string
|
|||
case results := <-cacheChan:
|
||||
if results == nil {
|
||||
// Fetch only if the cache miss occurs and Crawler is enabled
|
||||
if config.MetaSearchEnabled {
|
||||
if config.CrawlerEnabled {
|
||||
combinedResults = fetchForumResults(query, safe, lang, page)
|
||||
if len(combinedResults) > 0 {
|
||||
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
|
||||
|
@ -168,7 +164,7 @@ func getForumResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string
|
|||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
printDebug("Cache check timeout")
|
||||
if config.MetaSearchEnabled {
|
||||
if config.CrawlerEnabled {
|
||||
combinedResults = fetchForumResults(query, safe, lang, page)
|
||||
if len(combinedResults) > 0 {
|
||||
resultsCache.Set(cacheKey, convertToSearchResults(combinedResults))
|
||||
|
|
3
go.mod
3
go.mod
|
@ -17,7 +17,6 @@ require (
|
|||
github.com/blevesearch/bleve/v2 v2.4.4
|
||||
github.com/chromedp/cdproto v0.0.0-20241022234722-4d5d5faf59fb
|
||||
github.com/chromedp/chromedp v0.11.2
|
||||
github.com/fyne-io/image v0.1.1
|
||||
github.com/go-shiori/go-readability v0.0.0-20241012063810-92284fa8a71f
|
||||
golang.org/x/net v0.33.0
|
||||
)
|
||||
|
@ -56,11 +55,11 @@ require (
|
|||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.3.11 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -56,8 +56,6 @@ github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHG
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
|
@ -86,8 +84,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQiI8Ht/8=
|
||||
|
@ -115,8 +111,8 @@ github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMT
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
|
|
|
@ -18,21 +18,8 @@ func PerformBingImageSearch(query, safe, lang string, page int) ([]ImageSearchRe
|
|||
// Build the search URL
|
||||
searchURL := buildBingSearchURL(query, page)
|
||||
|
||||
// Create the HTTP request
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("creating request: %v", err)
|
||||
}
|
||||
|
||||
// Set User-Agent
|
||||
ImageUserAgent, err := GetUserAgent("Image-Search-Bing")
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("generating User-Agent: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", ImageUserAgent)
|
||||
|
||||
// Use MetaProxy if enabled
|
||||
resp, err := DoMetaProxyRequest(req)
|
||||
// Make the HTTP request
|
||||
resp, err := http.Get(searchURL)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
|
|
|
@ -87,15 +87,15 @@ func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSe
|
|||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Create the HTTP request
|
||||
// Make the HTTP request with User-Agent header
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("creating request: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", DeviantArtImageUserAgent)
|
||||
|
||||
// Perform the request using MetaProxy if enabled
|
||||
resp, err := DoMetaProxyRequest(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
|
@ -182,7 +182,7 @@ func PerformDeviantArtImageSearch(query, safe, lang string, page int) ([]ImageSe
|
|||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// Check if the number of results is zero
|
||||
// Check if the number of results is one or less
|
||||
if len(results) == 0 {
|
||||
return nil, duration, fmt.Errorf("no images found")
|
||||
}
|
||||
|
|
|
@ -18,21 +18,7 @@ func PerformImgurImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
var results []ImageSearchResult
|
||||
searchURL := buildImgurSearchURL(query, page)
|
||||
|
||||
// Create the HTTP request
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("creating request: %v", err)
|
||||
}
|
||||
|
||||
// Get the User-Agent string
|
||||
imgurUserAgent, err := GetUserAgent("Image-Search-Imgur")
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("getting user-agent: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", imgurUserAgent)
|
||||
|
||||
// Perform the HTTP request with MetaProxy if enabled
|
||||
resp, err := DoMetaProxyRequest(req)
|
||||
resp, err := http.Get(searchURL)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
|
@ -42,7 +28,6 @@ func PerformImgurImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
return nil, 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse the HTML document
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("loading HTML document: %v", err)
|
||||
|
@ -91,35 +76,12 @@ func PerformImgurImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
|
||||
duration := time.Since(startTime) // Calculate the duration
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, duration, fmt.Errorf("no images found")
|
||||
}
|
||||
|
||||
return results, duration, nil
|
||||
}
|
||||
|
||||
// scrapeImageFromImgurPage scrapes the image source from the Imgur page
|
||||
func scrapeImageFromImgurPage(pageURL string) string {
|
||||
req, err := http.NewRequest("GET", pageURL, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating request for page: %v\n", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get the User-Agent string
|
||||
imgurUserAgent, err := GetUserAgent("Image-Search-Imgur")
|
||||
if err == nil {
|
||||
req.Header.Set("User-Agent", imgurUserAgent)
|
||||
}
|
||||
|
||||
// Perform the request using MetaProxy if enabled
|
||||
var resp *http.Response
|
||||
if config.MetaProxyEnabled && metaProxyClient != nil {
|
||||
resp, err = metaProxyClient.Do(req)
|
||||
} else {
|
||||
client := &http.Client{}
|
||||
resp, err = client.Do(req)
|
||||
}
|
||||
resp, err := http.Get(pageURL)
|
||||
if err != nil {
|
||||
fmt.Printf("Error fetching page: %v\n", err)
|
||||
return ""
|
||||
|
|
|
@ -97,7 +97,7 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
|
||||
// Ensure count + offset is within acceptable limits
|
||||
if offset+resultsPerPage > 250 {
|
||||
return nil, 0, fmt.Errorf("count + offset must be lower than 250 for Qwant")
|
||||
return nil, 0, fmt.Errorf("count + offset must be lower than 250 for quant")
|
||||
}
|
||||
|
||||
if safe == "" {
|
||||
|
@ -113,21 +113,21 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
offset,
|
||||
safe)
|
||||
|
||||
// Create the HTTP request
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
req, err := http.NewRequest("GET", apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("creating request: %v", err)
|
||||
}
|
||||
|
||||
// Get the User-Agent string
|
||||
ImageUserAgent, err := GetUserAgent("Image-Search-Quant")
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("getting user-agent: %v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("User-Agent", ImageUserAgent)
|
||||
|
||||
// Perform the request with MetaProxy if enabled
|
||||
resp, err := DoMetaProxyRequest(req)
|
||||
req.Header.Set("User-Agent", ImageUserAgent) // Quant seems to not like some specific User-Agent strings
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("making request: %v", err)
|
||||
}
|
||||
|
@ -137,13 +137,11 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
return nil, 0, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse the API response
|
||||
var apiResp QwantAPIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||
return nil, 0, fmt.Errorf("decoding response: %v", err)
|
||||
}
|
||||
|
||||
// Process the results
|
||||
var wg sync.WaitGroup
|
||||
results := make([]ImageSearchResult, len(apiResp.Data.Result.Items))
|
||||
|
||||
|
@ -176,9 +174,5 @@ func PerformQwantImageSearch(query, safe, lang string, page int) ([]ImageSearchR
|
|||
|
||||
duration := time.Since(startTime) // Calculate the duration
|
||||
|
||||
if len(results) == 0 {
|
||||
return nil, duration, fmt.Errorf("no images found")
|
||||
}
|
||||
|
||||
return results, duration, nil
|
||||
}
|
||||
|
|
39
images.go
39
images.go
|
@ -10,23 +10,12 @@ import (
|
|||
|
||||
var imageSearchEngines []SearchEngine
|
||||
|
||||
var allImageSearchEngines = []SearchEngine{
|
||||
{Name: "Qwant", Func: wrapImageSearchFunc(PerformQwantImageSearch)},
|
||||
{Name: "Bing", Func: wrapImageSearchFunc(PerformBingImageSearch)},
|
||||
{Name: "DeviantArt", Func: wrapImageSearchFunc(PerformDeviantArtImageSearch)},
|
||||
// {Name: "Imgur", Func: wrapImageSearchFunc(PerformImgurImageSearch), Weight: 4}, // example
|
||||
}
|
||||
|
||||
func initImageEngines() {
|
||||
imageSearchEngines = nil
|
||||
|
||||
for _, engineName := range config.MetaSearch.Image {
|
||||
for _, candidate := range allImageSearchEngines {
|
||||
if candidate.Name == engineName {
|
||||
imageSearchEngines = append(imageSearchEngines, candidate)
|
||||
break
|
||||
}
|
||||
}
|
||||
func init() {
|
||||
imageSearchEngines = []SearchEngine{
|
||||
{Name: "Qwant", Func: wrapImageSearchFunc(PerformQwantImageSearch)},
|
||||
{Name: "Bing", Func: wrapImageSearchFunc(PerformBingImageSearch)},
|
||||
{Name: "DeviantArt", Func: wrapImageSearchFunc(PerformDeviantArtImageSearch)},
|
||||
//{Name: "Imgur", Func: wrapImageSearchFunc(PerformImgurImageSearch), Weight: 4}, // Image proxy not working
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,7 +44,7 @@ func handleImageSearch(w http.ResponseWriter, r *http.Request, settings UserSett
|
|||
data := map[string]interface{}{
|
||||
"Results": combinedResults,
|
||||
"Query": query,
|
||||
"Fetched": FormatElapsedTime(elapsedTime),
|
||||
"Fetched": fmt.Sprintf("%.2f %s", elapsedTime.Seconds(), Translate("seconds")),
|
||||
"Page": page,
|
||||
"HasPrevPage": page > 1,
|
||||
"HasNextPage": len(combinedResults) >= 50,
|
||||
|
@ -97,7 +86,7 @@ func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string
|
|||
select {
|
||||
case results := <-cacheChan:
|
||||
if results == nil {
|
||||
if config.MetaSearchEnabled {
|
||||
if config.CrawlerEnabled {
|
||||
combinedResults = fetchImageResults(query, safe, lang, page, synchronous)
|
||||
if len(combinedResults) > 0 {
|
||||
combinedResults = filterValidImages(combinedResults)
|
||||
|
@ -107,12 +96,12 @@ func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string
|
|||
printDebug("Crawler disabled; skipping fetching from image search engines.")
|
||||
}
|
||||
} else {
|
||||
_, _, imageResults, _, _ := convertToSpecificResults(results)
|
||||
_, _, imageResults, _ := convertToSpecificResults(results)
|
||||
combinedResults = filterValidImages(imageResults)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
printDebug("Cache check timeout")
|
||||
if config.MetaSearchEnabled {
|
||||
if config.CrawlerEnabled {
|
||||
combinedResults = fetchImageResults(query, safe, lang, page, synchronous)
|
||||
if len(combinedResults) > 0 {
|
||||
combinedResults = filterValidImages(combinedResults)
|
||||
|
@ -129,8 +118,8 @@ func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string
|
|||
func fetchImageResults(query, safe, lang string, page int, synchronous bool) []ImageSearchResult {
|
||||
var results []ImageSearchResult
|
||||
|
||||
// Check if MetaSearchEnabled is false
|
||||
if !config.MetaSearchEnabled {
|
||||
// Check if CrawlerEnabled is false
|
||||
if !config.CrawlerEnabled {
|
||||
printDebug("Crawler is disabled; skipping image search engine fetching.")
|
||||
return results
|
||||
}
|
||||
|
@ -174,7 +163,7 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
|
|||
if config.DriveCacheEnabled {
|
||||
// Cache the thumbnail image asynchronously
|
||||
go func(imgResult ImageSearchResult) {
|
||||
_, success, err := cacheImage(imgResult.Thumb, imgResult.ID, "thumb")
|
||||
_, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true)
|
||||
if err != nil || !success {
|
||||
printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err)
|
||||
removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID)
|
||||
|
@ -233,7 +222,7 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool) []I
|
|||
if config.DriveCacheEnabled {
|
||||
// Cache the thumbnail image asynchronously
|
||||
go func(imgResult ImageSearchResult) {
|
||||
_, success, err := cacheImage(imgResult.Thumb, imgResult.ID, "thumb")
|
||||
_, success, err := cacheImage(imgResult.Thumb, imgResult.ID, true)
|
||||
if err != nil || !success {
|
||||
printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err)
|
||||
removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID)
|
||||
|
|
26
init.go
26
init.go
|
@ -13,16 +13,10 @@ func main() {
|
|||
portFlag := flag.Int("port", 0, "Port number to run the application (overrides config)")
|
||||
domainFlag := flag.String("domain", "", "Domain address for the application (overrides config)")
|
||||
skipConfigFlag := flag.Bool("skip-config-check", false, "Skip interactive prompts and load config.ini")
|
||||
configFlag := flag.String("config", "", "Path to configuration file (overrides default)")
|
||||
|
||||
// Parse command-line flags
|
||||
flag.Parse()
|
||||
|
||||
// Override global configFilePath if --config flag is provided
|
||||
if *configFlag != "" {
|
||||
configFilePath = *configFlag
|
||||
}
|
||||
|
||||
if *skipConfigFlag {
|
||||
// Skip interactive configuration
|
||||
if _, err := os.Stat(configFilePath); err == nil {
|
||||
|
@ -66,24 +60,11 @@ func main() {
|
|||
}
|
||||
config.PeerID = hostID
|
||||
|
||||
if config.CrawlerProxyEnabled || config.MetaProxyEnabled {
|
||||
InitProxies()
|
||||
}
|
||||
|
||||
// Initiate Browser Agent updater
|
||||
if config.MetaSearchEnabled || config.IndexerEnabled {
|
||||
if config.CrawlerEnabled || config.IndexerEnabled {
|
||||
go periodicAgentUpdate()
|
||||
}
|
||||
|
||||
// Load List of Meta Search Engines
|
||||
if config.MetaSearchEnabled {
|
||||
initTextEngines()
|
||||
initImageEngines()
|
||||
initFileEngines()
|
||||
initPipedInstances()
|
||||
initMusicEngines()
|
||||
}
|
||||
|
||||
InitializeLanguage("en") // Initialize language before generating OpenSearch
|
||||
generateOpenSearchXML(config)
|
||||
|
||||
|
@ -143,6 +124,11 @@ func main() {
|
|||
|
||||
webCrawlerInit()
|
||||
|
||||
// No longer needed as crawled data are indexed imidietly
|
||||
// // Start periodic indexing (every 2 minutes)
|
||||
// dataFilePath := filepath.Join(config.DriveCache.Path, "data_to_index.txt")
|
||||
// startPeriodicIndexing(dataFilePath, 2*time.Minute)
|
||||
|
||||
printInfo("Indexer is enabled.")
|
||||
} else {
|
||||
printInfo("Indexer is disabled.")
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Soek vir nuwe resultate"
|
||||
msgstr "Soek vir nuwe resultate..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Vorige"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Volgende"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Verkry in %s"
|
||||
msgstr "Verkry in %s sekondes"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Aantal saaiers"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Jy is binne "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "meter van hierdie punt af"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekondes"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisekondes"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "تورنتات"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "جاري البحث عن نتائج جديدة"
|
||||
msgstr "جاري البحث عن نتائج جديدة..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "السابق"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "التالي"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "تم التحميل في %s"
|
||||
msgstr "تم التحميل في %s ثوانٍ"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "عدد المزودين"
|
||||
|
@ -198,9 +198,3 @@ msgstr "أنت على بعد "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "أمتار من هذه النقطة"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "ثواني"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "ميلي ثانية"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Торэнты"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Пошук новых вынікаў"
|
||||
msgstr "Пошук новых вынікаў..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Папярэдняе"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Наступнае"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Загружана за %s"
|
||||
msgstr "Загружана за %s секунд"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Па колькасці сейдэраў"
|
||||
|
@ -198,9 +198,4 @@ msgstr "Вы знаходзіцеся на адлегласці"
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "метраў ад гэтага пункта"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Секунды"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Мілісекунды"
|
||||
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Торенти"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Търсят се нови резултати"
|
||||
msgstr "Търсят се нови резултати..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Предишен"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Следващ"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Заредено за %s"
|
||||
msgstr "Заредено за %s секунди"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Сийдъри (качване)"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Намирате се на "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "метра от тази точка"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Секунди"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Милисекунди"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Cercant nous resultats"
|
||||
msgstr "Cercant nous resultats..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Anterior"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Següent"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Recuperat en %s"
|
||||
msgstr "Recuperat en %s segons"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Ordena per fonts"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Ets a "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metres d'aquest punt"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Segons"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Mil·lisegons"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrenty"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Hledám nové výsledky"
|
||||
msgstr "Hledám nové výsledky..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Předchozí"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Další"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Načteno za %s"
|
||||
msgstr "Načteno za %s sekund"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Počet seedů"
|
||||
|
@ -197,9 +197,4 @@ msgid "you_are_within"
|
|||
msgstr "Jste v dosahu "
|
||||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metrů od tohoto bodu"
|
||||
msgid "seconds"
|
||||
msgstr "Sekundy"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekundy"
|
||||
msgstr "metrů od tohoto bodu"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrenter"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Søger efter nye resultater"
|
||||
msgstr "Søger efter nye resultater..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Forrige"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Næste"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Hentet på %s"
|
||||
msgstr "Hentet på %s sekunder"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sorter efter seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Du er inden for "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "meter fra dette punkt"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekunder"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisekunder"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Suche nach neuen Ergebnissen"
|
||||
msgstr "Suche nach neuen Ergebnissen..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Vorherige"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Nächste"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Abgerufen in %s"
|
||||
msgstr "Abgerufen in %s Sekunden"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sortieren nach Seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Sie befinden sich innerhalb von "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "Metern von diesem Punkt entfernt"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekunden"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisekunden"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Αναζήτηση νέων αποτελεσμάτων"
|
||||
msgstr "Αναζήτηση νέων αποτελεσμάτων..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Προηγούμενο"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Επόμενο"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Ανακτήθηκε σε %s"
|
||||
msgstr "Ανακτήθηκε σε %s δευτερόλεπτα"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Ταξινόμηση κατά seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Βρίσκεστε εντός "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "μέτρων από αυτό το σημείο"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Δευτερόλεπτα"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Χιλιοστά του δευτερολέπτου"
|
||||
|
|
|
@ -65,7 +65,7 @@ msgid "site_name"
|
|||
msgstr "QGato"
|
||||
|
||||
msgid "site_description"
|
||||
msgstr "A open-source private search engine."
|
||||
msgstr "QGato - Private & Open"
|
||||
|
||||
msgid "site_tags"
|
||||
msgstr "search, qgato, spitfire"
|
||||
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Searching for new results"
|
||||
msgstr "Searching for new results..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Previous"
|
||||
|
@ -116,13 +116,7 @@ msgid "next"
|
|||
msgstr "Next"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Fetched in %s"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "seconds"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "milliseconds"
|
||||
msgstr "Fetched in %s seconds"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Number of Seeders"
|
||||
|
@ -204,9 +198,3 @@ msgstr "You are within "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "meters from this point"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Seconds"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milliseconds"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torentoj"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Serĉante novajn rezultojn"
|
||||
msgstr "Serĉante novajn rezultojn..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Antaŭa"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Sekva"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Prenita en %s"
|
||||
msgstr "Prenita en %s sekundoj"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Ordigi laŭ semantoj"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Vi estas ene de "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metroj de ĉi tiu punkto"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekundoj"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekundoj"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Buscando nuevos resultados"
|
||||
msgstr "Buscando nuevos resultados..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Anterior"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Siguiente"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Obtenido en %s"
|
||||
msgstr "Obtenido en %s segundos"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Ordenar por seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Estás dentro de "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metros de este punto"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Segundos"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisegundos"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrendid"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Otsitakse uusi tulemusi"
|
||||
msgstr "Otsitakse uusi tulemusi..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Eelmine"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Järgmine"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Laaditud %s"
|
||||
msgstr "Laaditud %s sekundiga"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sorteeri külvajate järgi"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Olete "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "meetri kaugusel sellest punktist"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekundit"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisekundit"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "تورنتها"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "در حال جستجوی نتایج جدید"
|
||||
msgstr "در حال جستجوی نتایج جدید..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "قبلی"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "بعدی"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "بازیابی شده در %s"
|
||||
msgstr "بازیابی شده در %s ثانیه"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "مرتبسازی بر اساس سیدرها"
|
||||
|
@ -198,9 +198,3 @@ msgstr "شما در فاصله "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "متری از این نقطه قرار دارید"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "ثانیه"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "میلیثانیه"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrentit"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Haetaan uusia tuloksia"
|
||||
msgstr "Haetaan uusia tuloksia..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Edellinen"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Seuraava"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Haettu %s"
|
||||
msgstr "Haettu %s sekunnissa"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Lajittele lähettäjien mukaan"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Olet "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metrin päässä tästä pisteestä"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekuntia"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisekuntia"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Recherche de nouveaux résultats"
|
||||
msgstr "Recherche de nouveaux résultats..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Précédent"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Suivant"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Récupéré en %s"
|
||||
msgstr "Récupéré en %s secondes"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Trier par seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Vous êtes à "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "mètres de ce point"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Secondes"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisecondes"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "टोरेंट्स"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "नए परिणामों की खोज कर रहे हैं"
|
||||
msgstr "नए परिणामों की खोज कर रहे हैं..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "पिछला"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "अगला"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "%s"
|
||||
msgstr "%s सेकंड में प्राप्त किया गया"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "सीडर्स के अनुसार छांटें"
|
||||
|
@ -198,9 +198,3 @@ msgstr "आप यहाँ हैं: "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "मीटर इस बिंदु से दूर"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "सेकंड"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "मिलीसेकंड"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrenti"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Traže se novi rezultati"
|
||||
msgstr "Traže se novi rezultati..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Prethodno"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Sljedeće"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Dohvaćeno za %s"
|
||||
msgstr "Dohvaćeno za %s sekundi"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sjeme (najviše)"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Nalazite se unutar "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metara od ove točke"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekunde"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekunde"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Új találatok keresése"
|
||||
msgstr "Új találatok keresése..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Előző"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Következő"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Lekérve %s"
|
||||
msgstr "Lekérve %s másodperc alatt"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Rendezés seederek szerint"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Ön itt van: "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "méterre ettől a ponttól"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Másodperc"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milliszekundum"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Թորրենտներ"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Նոր արդյունքներ որոնվում են"
|
||||
msgstr "Նոր արդյունքներ որոնվում են..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Նախորդը"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Հաջորդը"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Բեռնված է %s"
|
||||
msgstr "Բեռնված է %s վայրկյանում"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Ներբեռնում (արտահանող)"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Դուք գտնվում եք "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "մետր հեռավորության վրա այս կետից"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Վայրկյաններ"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Միլիվայրկյաններ"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrent"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Mencari hasil baru"
|
||||
msgstr "Mencari hasil baru..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Sebelumnya"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Berikutnya"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Ditemukan dalam %s"
|
||||
msgstr "Ditemukan dalam %s detik"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Urutkan berdasarkan seeder"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Anda berada dalam jarak "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "meter dari titik ini"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Detik"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milidetik"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrent"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Ricerca di nuovi risultati"
|
||||
msgstr "Ricerca di nuovi risultati..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Precedente"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Successivo"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Ottenuto in %s"
|
||||
msgstr "Ottenuto in %s secondi"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Ordina per seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Sei entro "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metri da questo punto"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Secondi"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisecondi"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "טורנטים"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "מחפש תוצאות חדשות"
|
||||
msgstr "מחפש תוצאות חדשות..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "הקודם"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "הבא"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "הובא ב-%s"
|
||||
msgstr "הובא ב-%s שניות"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "מיון לפי משתפים"
|
||||
|
@ -198,9 +198,3 @@ msgstr "אתם נמצאים במרחק של "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "מטרים מהנקודה הזו"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "שניות"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "אלפיות שניה"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "トレント"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "新しい結果を検索中"
|
||||
msgstr "新しい結果を検索中..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "前"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "次"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "%s"
|
||||
msgstr "%s 秒で取得"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "シーダーで並べ替え"
|
||||
|
@ -198,9 +198,3 @@ msgstr "あなたは "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "メートル以内の位置にいます"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "ミリ秒"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "토렌트"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "새로운 결과를 검색 중"
|
||||
msgstr "새로운 결과를 검색 중..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "이전"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "다음"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "%s"
|
||||
msgstr "%s초 만에 가져옴"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "시더 기준 정렬"
|
||||
|
@ -198,9 +198,3 @@ msgstr "당신은 이 안에 있습니다: "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "미터 떨어진 지점"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "초"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "밀리초"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrentai"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Ieškoma naujų rezultatų"
|
||||
msgstr "Ieškoma naujų rezultatų..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Ankstesnis"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Kitas"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Gauta per %s"
|
||||
msgstr "Gauta per %s sekundes"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Rikiuoti pagal siuntėjus"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Jūs esate "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metrų nuo šio taško"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekundės"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekundės"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
msgid "settings_title"
|
||||
msgid "settings_title"
|
||||
msgstr "Iestatījumi"
|
||||
|
||||
msgid "settings"
|
||||
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torenti"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Meklē jaunus rezultātus"
|
||||
msgstr "Meklē jaunus rezultātus..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Iepriekšējais"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Nākamais"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Iegūts %s"
|
||||
msgstr "Iegūts %s sekundēs"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Kārtot pēc sējējiem"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Jūs atrodaties "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metru attālumā no šī punkta"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekundes"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekundes"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Nieuwe resultaten zoeken"
|
||||
msgstr "Nieuwe resultaten zoeken..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Vorige"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Volgende"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Opgehaald in %s"
|
||||
msgstr "Opgehaald in %s seconden"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sorteer op seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Je bevindt je binnen "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "meter van dit punt"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Seconden"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milliseconden"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrenter"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Søker etter nye resultater"
|
||||
msgstr "Søker etter nye resultater..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Forrige"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Neste"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Hentet på %s"
|
||||
msgstr "Hentet på %s sekunder"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sorter etter seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Du er innenfor "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "meter fra dette punktet"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekunder"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisekunder"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrenty"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Wyszukiwanie nowych wyników"
|
||||
msgstr "Wyszukiwanie nowych wyników..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Poprzednie"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Następne"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Pobrano w %s"
|
||||
msgstr "Pobrano w %s sekund"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Liczba seedów"
|
||||
|
@ -197,10 +197,4 @@ msgid "you_are_within"
|
|||
msgstr "Znajdujesz się w odległości "
|
||||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metrów od tego punktu"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekundy"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekundy"
|
||||
msgstr "metrów od tego punktu"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Procurando por novos resultados"
|
||||
msgstr "Procurando por novos resultados..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Anterior"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Próximo"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Obtido em %s"
|
||||
msgstr "Obtido em %s segundos"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Ordenar por seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Você está dentro de "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metros deste ponto"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Segundos"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milissegundos"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrenturi"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Caut rezultate noi"
|
||||
msgstr "Caut rezultate noi..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Anterior"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Următorul"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Obținut în %s"
|
||||
msgstr "Obținut în %s secunde"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sortează după seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Te afli la "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metri de acest punct"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Secunde"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisecunde"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Торренты"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Идёт поиск новых результатов"
|
||||
msgstr "Идёт поиск новых результатов..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Предыдущий"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Следующий"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Получено за %s"
|
||||
msgstr "Получено за %s секунд"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Сортировать по сидерам"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Вы находитесь в "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "метрах от этой точки"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Секунды"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Миллисекунды"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrenty"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Hľadám nové výsledky"
|
||||
msgstr "Hľadám nové výsledky..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Predchádzajúce"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Ďalšie"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Načítané za %s"
|
||||
msgstr "Načítané za %s sekúnd"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Zoradiť podľa seedrov"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Nachádzate sa vo vzdialenosti "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metrov od tohto bodu"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekundy"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekundy"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrenti"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Iskanje novih rezultatov"
|
||||
msgstr "Iskanje novih rezultatov..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Prejšnje"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Naslednje"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Pridobljeno v %s"
|
||||
msgstr "Pridobljeno v %s sekundah"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Razvrsti po seederjih"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Nahajate se znotraj "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metrov od te točke"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekunde"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekunde"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Торенти"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Тражење нових резултата"
|
||||
msgstr "Тражење нових резултата..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Претходно"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Следеће"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Преузето за %s"
|
||||
msgstr "Преузето за %s секунди"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Сортирај по сеедерима"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Налазите се на удаљености од "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "метара од ове тачке"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Секунди"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Милисекунде"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Söker efter nya resultat"
|
||||
msgstr "Söker efter nya resultat..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Föregående"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Nästa"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Hämtad på %s"
|
||||
msgstr "Hämtad på %s sekunder"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sortera efter seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Du är inom "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "meter från denna punkt"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekunder"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Millisekunder"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torenti"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Inatafuta matokeo mapya"
|
||||
msgstr "Inatafuta matokeo mapya..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Ya awali"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Uko ndani ya "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "mita kutoka eneo hili"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Sekunde"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisekunde"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "ทอร์เรนต์"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "กำลังค้นหาผลลัพธ์ใหม่"
|
||||
msgstr "กำลังค้นหาผลลัพธ์ใหม่..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "ก่อนหน้า"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "ถัดไป"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "ดึงข้อมูลใน %s"
|
||||
msgstr "ดึงข้อมูลใน %s วินาที"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "จัดเรียงตามซีดเดอร์"
|
||||
|
@ -198,9 +198,3 @@ msgstr "คุณอยู่ภายในระยะ "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "เมตรจากจุดนี้"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "วินาที"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "มิลลิวินาที"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Mga Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Naghahanap ng mga bagong resulta"
|
||||
msgstr "Naghahanap ng mga bagong resulta..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Nakaraan"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Susunod"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Nakuha sa %s"
|
||||
msgstr "Nakuha sa %s segundo"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Ayusin ayon sa seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Ikaw ay nasa loob ng "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metro mula sa puntong ito"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Segundo"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milyasegundo"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrentler"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Yeni sonuçlar aranıyor"
|
||||
msgstr "Yeni sonuçlar aranıyor..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Önceki"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Sonraki"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "%s"
|
||||
msgstr "%s saniyede alındı"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Seeders'a göre sırala"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Şuradasınız: "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "metre bu noktadan"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Saniye"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Milisaniye"
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Торренти"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Шукаю нові результати"
|
||||
msgstr "Шукаю нові результати..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Попередній"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Наступний"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Отримано за %s"
|
||||
msgstr "Отримано за %s секунд"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Сортувати за сідерами"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Ви перебуваєте в межах "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "метрів від цієї точки"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Секунди"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Мілісекунди"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "Torrents"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "Đang tìm kiếm kết quả mới"
|
||||
msgstr "Đang tìm kiếm kết quả mới..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "Trước"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "Tiếp theo"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "Đã tìm trong %s"
|
||||
msgstr "Đã tìm trong %s giây"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "Sắp xếp theo seeders"
|
||||
|
@ -198,9 +198,3 @@ msgstr "Bạn đang ở trong phạm vi "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "mét từ điểm này"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "Giây"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "Mili giây"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "种子"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "正在搜索新结果"
|
||||
msgstr "正在搜索新结果..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "上一页"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "下一页"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "%s"
|
||||
msgstr "%s 秒内获取"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "排序:上传者"
|
||||
|
@ -198,9 +198,3 @@ msgstr "您距离此点 "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "米"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "毫秒"
|
||||
|
|
|
@ -107,7 +107,7 @@ msgid "torrents"
|
|||
msgstr "種子"
|
||||
|
||||
msgid "searching_for_new_results"
|
||||
msgstr "正在搜尋新結果"
|
||||
msgstr "正在搜尋新結果..."
|
||||
|
||||
msgid "previous"
|
||||
msgstr "上一頁"
|
||||
|
@ -116,7 +116,7 @@ msgid "next"
|
|||
msgstr "下一頁"
|
||||
|
||||
msgid "fetched_in"
|
||||
msgstr "已於 %s"
|
||||
msgstr "已於 %s 秒內加載"
|
||||
|
||||
msgid "sort_seeders"
|
||||
msgstr "排序(種子數量)"
|
||||
|
@ -198,9 +198,3 @@ msgstr "您在 "
|
|||
|
||||
msgid "meters_from_point"
|
||||
msgstr "公尺範圍內"
|
||||
|
||||
msgid "seconds"
|
||||
msgstr "秒"
|
||||
|
||||
msgid "milliseconds"
|
||||
msgstr "毫秒"
|
||||
|
|
25
main.go
25
main.go
|
@ -164,8 +164,6 @@ func handleSearch(w http.ResponseWriter, r *http.Request) {
|
|||
handleImageSearch(w, r, settings, query, page)
|
||||
case "video":
|
||||
handleVideoSearch(w, settings, query, page)
|
||||
case "music":
|
||||
handleMusicSearch(w, settings, query, page)
|
||||
case "map":
|
||||
handleMapSearch(w, settings, query)
|
||||
case "forum":
|
||||
|
@ -228,7 +226,7 @@ func runServer() {
|
|||
w.Header().Set("Content-Type", "application/opensearchdescription+xml")
|
||||
http.ServeFile(w, r, "static/opensearch.xml")
|
||||
})
|
||||
printInfo("Website is enabled.")
|
||||
printInfo("Website functionality enabled.")
|
||||
} else {
|
||||
// Redirect all website routes to a "service disabled" handler
|
||||
http.HandleFunc("/static/", handleWebsiteDisabled)
|
||||
|
@ -240,7 +238,7 @@ func runServer() {
|
|||
http.HandleFunc("/image_status", handleWebsiteDisabled)
|
||||
http.HandleFunc("/privacy", handleWebsiteDisabled)
|
||||
http.HandleFunc("/opensearch.xml", handleWebsiteDisabled)
|
||||
printInfo("Website is disabled.")
|
||||
printInfo("Website functionality disabled.")
|
||||
}
|
||||
|
||||
if config.NodesEnabled {
|
||||
|
@ -254,7 +252,7 @@ func runServer() {
|
|||
func handleWebsiteDisabled(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_, _ = w.Write([]byte("The website is currently disabled."))
|
||||
_, _ = w.Write([]byte("The website functionality is currently disabled."))
|
||||
}
|
||||
|
||||
func handlePrivacyPage(w http.ResponseWriter, r *http.Request) {
|
||||
|
@ -282,5 +280,20 @@ func handlePrivacyPage(w http.ResponseWriter, r *http.Request) {
|
|||
LanguageOptions: languageOptions,
|
||||
}
|
||||
|
||||
renderTemplate(w, "privacy.html", toMap(data))
|
||||
// Parse the template
|
||||
tmpl, err := template.New("privacy.html").ParseFiles("templates/privacy.html")
|
||||
if err != nil {
|
||||
log.Printf("Error parsing template: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set the response content type
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
// Execute the template
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
log.Printf("Error executing template: %v", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
|
15
map.go
15
map.go
|
@ -5,6 +5,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NominatimResponse struct {
|
||||
|
@ -58,7 +59,7 @@ func geocodeQuery(query string) (latitude, longitude string, found bool, err err
|
|||
|
||||
func handleMapSearch(w http.ResponseWriter, settings UserSettings, query string) {
|
||||
// Start measuring the time for geocoding the query
|
||||
//startTime := time.Now()
|
||||
startTime := time.Now()
|
||||
|
||||
// Geocode the query to get coordinates
|
||||
latitude, longitude, found, err := geocodeQuery(query)
|
||||
|
@ -69,15 +70,15 @@ func handleMapSearch(w http.ResponseWriter, settings UserSettings, query string)
|
|||
}
|
||||
|
||||
// Measure the elapsed time for geocoding
|
||||
//elapsed := time.Since(startTime)
|
||||
elapsedTime := time.Since(startTime)
|
||||
|
||||
// Prepare the data to pass to the template
|
||||
data := map[string]interface{}{
|
||||
"Query": query,
|
||||
"Latitude": latitude,
|
||||
"Longitude": longitude,
|
||||
"Found": found,
|
||||
//"Fetched": FormatElapsedTime(elapsed), // not used in map tab
|
||||
"Query": query,
|
||||
"Latitude": latitude,
|
||||
"Longitude": longitude,
|
||||
"Found": found,
|
||||
"Fetched": fmt.Sprintf("%.2f %s", elapsedTime.Seconds(), Translate("seconds")),
|
||||
"Theme": settings.Theme,
|
||||
"Safe": settings.SafeSearch,
|
||||
"IsThemeDark": settings.IsThemeDark,
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
// music-bandcamp.go - Bandcamp specific implementation
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func SearchBandcamp(query string, page int) ([]MusicResult, error) {
|
||||
baseURL := "https://bandcamp.com/search?"
|
||||
params := url.Values{
|
||||
"q": []string{query},
|
||||
"page": []string{fmt.Sprintf("%d", page)},
|
||||
}
|
||||
|
||||
resp, err := http.Get(baseURL + params.Encode())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse HTML: %v", err)
|
||||
}
|
||||
|
||||
var results []MusicResult
|
||||
|
||||
doc.Find("li.searchresult").Each(func(i int, s *goquery.Selection) {
|
||||
// Extract the item type
|
||||
itemType := strings.ToLower(strings.TrimSpace(s.Find("div.itemtype").Text()))
|
||||
|
||||
// Skip if the item is not an album or track
|
||||
if itemType != "album" && itemType != "track" {
|
||||
return
|
||||
}
|
||||
|
||||
result := MusicResult{Source: "Bandcamp"}
|
||||
|
||||
// URL extraction
|
||||
if urlSel := s.Find("div.itemurl a"); urlSel.Length() > 0 {
|
||||
result.URL = strings.TrimSpace(urlSel.Text())
|
||||
}
|
||||
|
||||
// Title extraction
|
||||
if titleSel := s.Find("div.heading a"); titleSel.Length() > 0 {
|
||||
result.Title = strings.TrimSpace(titleSel.Text())
|
||||
}
|
||||
|
||||
// Artist extraction
|
||||
if artistSel := s.Find("div.subhead"); artistSel.Length() > 0 {
|
||||
result.Artist = strings.TrimSpace(artistSel.Text())
|
||||
}
|
||||
|
||||
// Thumbnail extraction
|
||||
if thumbSel := s.Find("div.art img"); thumbSel.Length() > 0 {
|
||||
result.Thumbnail, _ = thumbSel.Attr("src")
|
||||
}
|
||||
|
||||
// // Iframe URL construction
|
||||
// if linkHref, exists := s.Find("div.itemurl a").Attr("href"); exists {
|
||||
// if itemID := extractSearchItemID(linkHref); itemID != "" {
|
||||
// itemType := strings.ToLower(strings.TrimSpace(s.Find("div.itemtype").Text()))
|
||||
// result.IframeSrc = fmt.Sprintf(
|
||||
// "https://bandcamp.com/EmbeddedPlayer/%s=%s/size=large/bgcol=000/linkcol=fff/artwork=small",
|
||||
// itemType,
|
||||
// itemID,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
results = append(results, result)
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
|
@ -1,198 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
type SoundCloudTrack struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Permalink string `json:"permalink"`
|
||||
ArtworkURL string `json:"artwork_url"`
|
||||
Duration int `json:"duration"`
|
||||
User struct {
|
||||
Username string `json:"username"`
|
||||
Permalink string `json:"permalink"`
|
||||
} `json:"user"`
|
||||
Streams struct {
|
||||
HTTPMP3128URL string `json:"http_mp3_128_url"`
|
||||
} `json:"streams"`
|
||||
}
|
||||
|
||||
func SearchSoundCloud(query string, page int) ([]MusicResult, error) {
|
||||
clientID, err := extractClientID()
|
||||
if err != nil {
|
||||
return searchSoundCloudViaScraping(query, page)
|
||||
}
|
||||
|
||||
apiResults, err := searchSoundCloudViaAPI(query, clientID, page)
|
||||
if err == nil && len(apiResults) > 0 {
|
||||
return convertSoundCloudResults(apiResults), nil
|
||||
}
|
||||
|
||||
return searchSoundCloudViaScraping(query, page)
|
||||
}
|
||||
|
||||
func searchSoundCloudViaAPI(query, clientID string, page int) ([]SoundCloudTrack, error) {
|
||||
const limit = 10
|
||||
offset := (page - 1) * limit
|
||||
|
||||
apiUrl := fmt.Sprintf(
|
||||
"https://api-v2.soundcloud.com/search/tracks?q=%s&client_id=%s&limit=%d&offset=%d",
|
||||
url.QueryEscape(query),
|
||||
clientID,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
|
||||
resp, err := http.Get(apiUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Collection []SoundCloudTrack `json:"collection"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.Collection, nil
|
||||
}
|
||||
|
||||
func convertSoundCloudResults(tracks []SoundCloudTrack) []MusicResult {
|
||||
var results []MusicResult
|
||||
|
||||
for _, track := range tracks {
|
||||
thumbnail := strings.Replace(track.ArtworkURL, "large", "t500x500", 1)
|
||||
trackURL := fmt.Sprintf("https://soundcloud.com/%s/%s",
|
||||
track.User.Permalink,
|
||||
track.Permalink,
|
||||
)
|
||||
|
||||
results = append(results, MusicResult{
|
||||
Title: track.Title,
|
||||
Artist: track.User.Username,
|
||||
URL: trackURL,
|
||||
Thumbnail: thumbnail,
|
||||
//AudioURL: track.Streams.HTTPMP3128URL,
|
||||
Source: "SoundCloud",
|
||||
Duration: fmt.Sprintf("%d", track.Duration/1000),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func searchSoundCloudViaScraping(query string, page int) ([]MusicResult, error) {
|
||||
searchUrl := fmt.Sprintf("https://soundcloud.com/search/sounds?q=%s", url.QueryEscape(query))
|
||||
resp, err := http.Get(searchUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []MusicResult
|
||||
doc.Find("li.searchList__item").Each(func(i int, s *goquery.Selection) {
|
||||
titleElem := s.Find("a.soundTitle__title")
|
||||
artistElem := s.Find("a.soundTitle__username")
|
||||
artworkElem := s.Find(".sound__coverArt")
|
||||
|
||||
title := strings.TrimSpace(titleElem.Text())
|
||||
artist := strings.TrimSpace(artistElem.Text())
|
||||
href, _ := titleElem.Attr("href")
|
||||
thumbnail, _ := artworkElem.Find("span.sc-artwork").Attr("style")
|
||||
|
||||
if thumbnail != "" {
|
||||
if matches := regexp.MustCompile(`url\((.*?)\)`).FindStringSubmatch(thumbnail); len(matches) > 1 {
|
||||
thumbnail = strings.Trim(matches[1], `"`)
|
||||
}
|
||||
}
|
||||
|
||||
if title == "" || href == "" {
|
||||
return
|
||||
}
|
||||
|
||||
trackURL, err := url.Parse(href)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if trackURL.Host == "" {
|
||||
trackURL.Scheme = "https"
|
||||
trackURL.Host = "soundcloud.com"
|
||||
}
|
||||
|
||||
trackURL.Path = strings.ReplaceAll(trackURL.Path, "//", "/")
|
||||
fullURL := trackURL.String()
|
||||
|
||||
results = append(results, MusicResult{
|
||||
Title: title,
|
||||
Artist: artist,
|
||||
URL: fullURL,
|
||||
Thumbnail: thumbnail,
|
||||
Source: "SoundCloud",
|
||||
})
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func extractClientID() (string, error) {
|
||||
resp, err := http.Get("https://soundcloud.com/")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var clientID string
|
||||
doc.Find("script[src]").Each(func(i int, s *goquery.Selection) {
|
||||
if clientID != "" {
|
||||
return
|
||||
}
|
||||
|
||||
src, _ := s.Attr("src")
|
||||
if strings.Contains(src, "sndcdn.com/assets/") {
|
||||
resp, err := http.Get(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
re := regexp.MustCompile(`client_id:"([^"]+)"`)
|
||||
matches := re.FindSubmatch(body)
|
||||
if len(matches) > 1 {
|
||||
clientID = string(matches[1])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if clientID == "" {
|
||||
return "", fmt.Errorf("client_id not found")
|
||||
}
|
||||
return clientID, nil
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
)
|
||||
|
||||
func SearchSpotify(query string, page int) ([]MusicResult, error) {
|
||||
searchUrl := fmt.Sprintf("https://open.spotify.com/search/%s", url.PathEscape(query))
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", searchUrl, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
// Set user agent ?
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("received non-200 status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
doc, err := goquery.NewDocumentFromReader(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse document: %v", err)
|
||||
}
|
||||
|
||||
var results []MusicResult
|
||||
|
||||
// Find track elements
|
||||
doc.Find(`div[data-testid="tracklist-row"]`).Each(func(i int, s *goquery.Selection) {
|
||||
// Extract title
|
||||
title := s.Find(`div[data-testid="tracklist-row__title"] a`).Text()
|
||||
title = strings.TrimSpace(title)
|
||||
|
||||
// Extract artist
|
||||
artist := s.Find(`div[data-testid="tracklist-row__artist"] a`).First().Text()
|
||||
artist = strings.TrimSpace(artist)
|
||||
|
||||
// Extract duration
|
||||
duration := s.Find(`div[data-testid="tracklist-row__duration"]`).First().Text()
|
||||
duration = strings.TrimSpace(duration)
|
||||
|
||||
// Extract URL
|
||||
path, _ := s.Find(`div[data-testid="tracklist-row__title"] a`).Attr("href")
|
||||
fullUrl := fmt.Sprintf("https://open.spotify.com%s", path)
|
||||
|
||||
// Extract thumbnail
|
||||
thumbnail, _ := s.Find(`img[aria-hidden="false"]`).Attr("src")
|
||||
|
||||
if title != "" && artist != "" {
|
||||
results = append(results, MusicResult{
|
||||
Title: title,
|
||||
Artist: artist,
|
||||
URL: fullUrl,
|
||||
Duration: duration,
|
||||
Thumbnail: thumbnail,
|
||||
Source: "Spotify",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return results, nil
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type MusicAPIResponse struct {
|
||||
Items []struct {
|
||||
Title string `json:"title"`
|
||||
UploaderName string `json:"uploaderName"`
|
||||
Duration int `json:"duration"`
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
URL string `json:"url"`
|
||||
} `json:"items"` // Removed VideoID since we'll parse from URL
|
||||
}
|
||||
|
||||
func SearchMusicViaPiped(query string, page int) ([]MusicResult, error) {
|
||||
var lastError error
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
for _, instance := range pipedInstances {
|
||||
if disabledInstances[instance] {
|
||||
continue
|
||||
}
|
||||
|
||||
url := fmt.Sprintf(
|
||||
"https://%s/search?q=%s&filter=music_songs&page=%d",
|
||||
instance,
|
||||
url.QueryEscape(query),
|
||||
page,
|
||||
)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
printInfo("Disabling instance %s due to error: %v", instance, err)
|
||||
disabledInstances[instance] = true
|
||||
lastError = fmt.Errorf("request to %s failed: %w", instance, err)
|
||||
continue
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
var apiResp MusicAPIResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
||||
lastError = fmt.Errorf("failed to decode response from %s: %w", instance, err)
|
||||
continue
|
||||
}
|
||||
|
||||
return convertPipedToMusicResults(instance, apiResp), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all Piped instances failed, last error: %v", lastError)
|
||||
}
|
||||
|
||||
func convertPipedToMusicResults(instance string, resp MusicAPIResponse) []MusicResult {
|
||||
seen := make(map[string]bool)
|
||||
var results []MusicResult
|
||||
|
||||
for _, item := range resp.Items {
|
||||
// Extract video ID from URL
|
||||
u, err := url.Parse(item.URL)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
videoID := u.Query().Get("v")
|
||||
if videoID == "" || seen[videoID] {
|
||||
continue
|
||||
}
|
||||
seen[videoID] = true
|
||||
|
||||
results = append(results, MusicResult{
|
||||
Title: item.Title,
|
||||
Artist: item.UploaderName,
|
||||
URL: fmt.Sprintf("https://music.youtube.com%s", item.URL),
|
||||
Duration: formatDuration(item.Duration),
|
||||
Thumbnail: item.Thumbnail,
|
||||
Source: "YouTube Music",
|
||||
//AudioURL: fmt.Sprintf("https://%s/stream/%s", instance, videoID),
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
176
music.go
176
music.go
|
@ -1,176 +0,0 @@
|
|||
// music.go - Central music search handler
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MusicSearchEngine struct {
|
||||
Name string
|
||||
Func func(query string, page int) ([]MusicResult, error)
|
||||
}
|
||||
|
||||
var (
|
||||
musicSearchEngines []MusicSearchEngine
|
||||
cacheMutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
var allMusicSearchEngines = []MusicSearchEngine{
|
||||
{Name: "SoundCloud", Func: SearchSoundCloud},
|
||||
{Name: "YouTube", Func: SearchMusicViaPiped},
|
||||
{Name: "Bandcamp", Func: SearchBandcamp},
|
||||
//{Name: "Spotify", Func: SearchSpotify},
|
||||
}
|
||||
|
||||
func initMusicEngines() {
|
||||
// Initialize with all engines if no specific config
|
||||
musicSearchEngines = allMusicSearchEngines
|
||||
}
|
||||
|
||||
func handleMusicSearch(w http.ResponseWriter, settings UserSettings, query string, page int) {
|
||||
start := time.Now()
|
||||
|
||||
cacheKey := CacheKey{
|
||||
Query: query,
|
||||
Page: page,
|
||||
Type: "music",
|
||||
Lang: settings.SearchLanguage,
|
||||
Safe: settings.SafeSearch == "active",
|
||||
}
|
||||
|
||||
var results []MusicResult
|
||||
|
||||
if cached, found := resultsCache.Get(cacheKey); found {
|
||||
if musicResults, ok := convertCacheToMusicResults(cached); ok {
|
||||
results = musicResults
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
results = fetchMusicResults(query, page)
|
||||
if len(results) > 0 {
|
||||
resultsCache.Set(cacheKey, convertMusicResultsToCache(results))
|
||||
}
|
||||
}
|
||||
|
||||
go prefetchMusicPages(query, page)
|
||||
|
||||
elapsed := time.Since(start) // Calculate duration
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Results": results,
|
||||
"Query": query,
|
||||
"Page": page,
|
||||
"HasPrevPage": page > 1,
|
||||
"HasNextPage": len(results) >= 10, // Default page size
|
||||
"MusicServices": getMusicServiceNames(),
|
||||
"CurrentService": "all", // Default service
|
||||
"Theme": settings.Theme,
|
||||
"IsThemeDark": settings.IsThemeDark,
|
||||
"Trans": Translate,
|
||||
"Fetched": FormatElapsedTime(elapsed),
|
||||
}
|
||||
|
||||
renderTemplate(w, "music.html", data)
|
||||
}
|
||||
|
||||
// Helper to get music service names
|
||||
func getMusicServiceNames() []string {
|
||||
names := make([]string, len(allMusicSearchEngines))
|
||||
for i, engine := range allMusicSearchEngines {
|
||||
names[i] = engine.Name
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func convertMusicResultsToCache(results []MusicResult) []SearchResult {
|
||||
cacheResults := make([]SearchResult, len(results))
|
||||
for i, r := range results {
|
||||
cacheResults[i] = r
|
||||
}
|
||||
return cacheResults
|
||||
}
|
||||
|
||||
func convertCacheToMusicResults(cached []SearchResult) ([]MusicResult, bool) {
|
||||
results := make([]MusicResult, 0, len(cached))
|
||||
for _, item := range cached {
|
||||
if musicResult, ok := item.(MusicResult); ok {
|
||||
results = append(results, musicResult)
|
||||
} else {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return results, true
|
||||
}
|
||||
|
||||
func fetchMusicResults(query string, page int) []MusicResult {
|
||||
var results []MusicResult
|
||||
resultsChan := make(chan []MusicResult, len(musicSearchEngines))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, engine := range musicSearchEngines {
|
||||
wg.Add(1)
|
||||
go func(e MusicSearchEngine) {
|
||||
defer wg.Done()
|
||||
res, err := e.Func(query, page)
|
||||
if err == nil && len(res) > 0 {
|
||||
resultsChan <- res
|
||||
}
|
||||
}(engine)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsChan)
|
||||
}()
|
||||
|
||||
for res := range resultsChan {
|
||||
results = append(results, res...)
|
||||
if len(results) >= 50 { // Default max results
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return deduplicateResults(results)
|
||||
}
|
||||
|
||||
func prefetchMusicPages(query string, currentPage int) {
|
||||
for _, page := range []int{currentPage - 1, currentPage + 1} {
|
||||
if page < 1 {
|
||||
continue
|
||||
}
|
||||
cacheKey := CacheKey{
|
||||
Query: query,
|
||||
Page: page,
|
||||
Type: "music",
|
||||
}
|
||||
if _, found := resultsCache.Get(cacheKey); !found {
|
||||
go fetchMusicResults(query, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deduplicateResults(results []MusicResult) []MusicResult {
|
||||
seen := make(map[string]bool)
|
||||
var unique []MusicResult
|
||||
|
||||
for _, res := range results {
|
||||
if !seen[res.URL] {
|
||||
seen[res.URL] = true
|
||||
unique = append(unique, res)
|
||||
}
|
||||
}
|
||||
return unique
|
||||
}
|
||||
|
||||
// func generatePlayerHTML(result MusicResult) template.HTML {
|
||||
// if result.IframeSrc != "" {
|
||||
// return template.HTML(fmt.Sprintf(
|
||||
// `<iframe width="100%%" height="166" scrolling="no" frameborder="no" src="%s"></iframe>`,
|
||||
// result.IframeSrc,
|
||||
// ))
|
||||
// }
|
||||
// return template.HTML("")
|
||||
// }
|
7
node.go
7
node.go
|
@ -5,7 +5,7 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
@ -65,10 +65,7 @@ func sendMessage(serverAddr string, msg Message) error {
|
|||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response body: %v", err)
|
||||
}
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf("server error: %s", body)
|
||||
}
|
||||
|
||||
|
|
270
proxy.go
270
proxy.go
|
@ -1,270 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
// ProxyConfig holds configuration for a single proxy.
|
||||
type ProxyConfig struct {
|
||||
Address string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// ProxyClient provides an HTTP client pool for proxies.
|
||||
type ProxyClient struct {
|
||||
clients []*http.Client
|
||||
lock sync.Mutex
|
||||
index int
|
||||
}
|
||||
|
||||
// Package-level proxy clients
|
||||
var (
|
||||
metaProxyClient *ProxyClient
|
||||
crawlerProxyClient *ProxyClient
|
||||
)
|
||||
|
||||
// NewProxyClientPool creates a pool of HTTP clients with SOCKS5 proxies.
|
||||
func NewProxyClientPool(proxies []ProxyConfig, timeout time.Duration) (*ProxyClient, error) {
|
||||
if len(proxies) == 0 {
|
||||
return nil, fmt.Errorf("no proxies provided")
|
||||
}
|
||||
|
||||
clients := make([]*http.Client, len(proxies))
|
||||
|
||||
for i, pc := range proxies {
|
||||
var auth *proxy.Auth
|
||||
if pc.Username != "" || pc.Password != "" {
|
||||
auth = &proxy.Auth{
|
||||
User: pc.Username,
|
||||
Password: pc.Password,
|
||||
}
|
||||
}
|
||||
dialer, err := proxy.SOCKS5("tcp", pc.Address, auth, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SOCKS5 dialer for %s: %w", pc.Address, err)
|
||||
}
|
||||
|
||||
transport := &http.Transport{Dial: dialer.Dial}
|
||||
clients[i] = &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
return &ProxyClient{clients: clients}, nil
|
||||
}
|
||||
|
||||
// Do sends an HTTP request using the next proxy in the pool.
|
||||
func (p *ProxyClient) Do(req *http.Request) (*http.Response, error) {
|
||||
p.lock.Lock()
|
||||
client := p.clients[p.index]
|
||||
p.index = (p.index + 1) % len(p.clients)
|
||||
p.lock.Unlock()
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func (p *ProxyClient) GetProxy() string {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
if len(p.clients) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Round-robin proxy retrieval
|
||||
client := p.clients[p.index]
|
||||
p.index = (p.index + 1) % len(p.clients)
|
||||
|
||||
// Assume each client has a proxy string saved
|
||||
// Example implementation depends on how your proxies are configured
|
||||
proxyTransport, ok := client.Transport.(*http.Transport)
|
||||
if ok && proxyTransport.Proxy != nil {
|
||||
proxyURL, _ := proxyTransport.Proxy(nil)
|
||||
if proxyURL != nil {
|
||||
return proxyURL.String()
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ParseProxies parses the proxy strings in the format ADDRESS:PORT or ADDRESS:PORT:USER:PASSWORD.
|
||||
func ParseProxies(proxyStrings []string) []ProxyConfig {
|
||||
var proxies []ProxyConfig
|
||||
for _, proxyStr := range proxyStrings {
|
||||
parts := strings.Split(proxyStr, ":")
|
||||
switch len(parts) {
|
||||
case 2: // ADDRESS:PORT
|
||||
proxies = append(proxies, ProxyConfig{
|
||||
Address: fmt.Sprintf("%s:%s", parts[0], parts[1]),
|
||||
})
|
||||
case 4: // ADDRESS:PORT:USER:PASSWORD
|
||||
proxies = append(proxies, ProxyConfig{
|
||||
Address: fmt.Sprintf("%s:%s", parts[0], parts[1]),
|
||||
Username: parts[2],
|
||||
Password: parts[3],
|
||||
})
|
||||
default:
|
||||
fmt.Printf("Invalid proxy format: %s\n", proxyStr)
|
||||
}
|
||||
}
|
||||
return proxies
|
||||
}
|
||||
|
||||
// InitProxies initializes the proxy clients for Meta and Crawler proxies.
|
||||
func InitProxies() {
|
||||
// Initialize Meta Proxy Client
|
||||
if config.MetaProxyEnabled {
|
||||
metaProxies := ParseProxies(config.MetaProxies)
|
||||
client, err := NewProxyClientPool(metaProxies, 30*time.Second)
|
||||
if err != nil {
|
||||
if config.MetaProxyStrict {
|
||||
panic(fmt.Sprintf("Failed to initialize Meta proxies: %v", err))
|
||||
}
|
||||
fmt.Printf("Warning: Meta proxy initialization failed: %v\n", err)
|
||||
}
|
||||
metaProxyClient = client
|
||||
}
|
||||
|
||||
// Initialize Crawler Proxy Client
|
||||
if config.CrawlerProxyEnabled {
|
||||
crawlerProxies := ParseProxies(config.CrawlerProxies)
|
||||
client, err := NewProxyClientPool(crawlerProxies, 30*time.Second)
|
||||
if err != nil {
|
||||
if config.CrawlerProxyStrict {
|
||||
panic(fmt.Sprintf("Failed to initialize Crawler proxies: %v", err))
|
||||
}
|
||||
fmt.Printf("Warning: Crawler proxy initialization failed: %v\n", err)
|
||||
}
|
||||
crawlerProxyClient = client
|
||||
}
|
||||
}
|
||||
|
||||
// Doer is an interface so we can accept *http.Client or *ProxyClient for requests.
|
||||
type Doer interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
// DoProxyRequest handles “try direct, then proxy if needed,” with retries if proxy is used.
|
||||
//
|
||||
// - strict: if true, always try proxy first if enabled; if not available, do one direct attempt
|
||||
// - enabled: whether this type of proxy is turned on
|
||||
// - retryCount: how many times to retry with the proxy
|
||||
// - proxyClient: the pool of proxy connections
|
||||
func DoProxyRequest(req *http.Request, strict bool, enabled bool, retryCount int, proxyClient *ProxyClient) (*http.Response, error) {
|
||||
// 1) If !strict => try direct once first
|
||||
if !strict {
|
||||
resp, err := tryRequestOnce(req, http.DefaultClient)
|
||||
if isSuccessful(resp, err) {
|
||||
return resp, nil
|
||||
}
|
||||
// If direct fails => if proxy is enabled, retry
|
||||
if enabled && proxyClient != nil {
|
||||
resp, err = tryRequestWithRetry(req, proxyClient, retryCount)
|
||||
if isSuccessful(resp, err) {
|
||||
return resp, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed after direct & proxy attempts: %v", err)
|
||||
}
|
||||
return nil, fmt.Errorf("request failed direct, no valid proxy: %v", err)
|
||||
}
|
||||
|
||||
// 2) If strict => if proxy is enabled, try it up to “retryCount”
|
||||
if enabled && proxyClient != nil {
|
||||
resp, err := tryRequestWithRetry(req, proxyClient, retryCount)
|
||||
if isSuccessful(resp, err) {
|
||||
return resp, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed after %d proxy attempts: %v", retryCount, err)
|
||||
}
|
||||
|
||||
// If strict but no proxy => direct once
|
||||
resp, err := tryRequestOnce(req, http.DefaultClient)
|
||||
if isSuccessful(resp, err) {
|
||||
return resp, nil
|
||||
}
|
||||
return nil, fmt.Errorf("direct request failed in strict mode, no proxy: %v", err)
|
||||
}
|
||||
|
||||
// Helper Wrapper functions for DoProxyRequest()
|
||||
func DoMetaProxyRequest(req *http.Request) (*http.Response, error) {
|
||||
return DoProxyRequest(
|
||||
req,
|
||||
config.MetaProxyStrict,
|
||||
config.MetaProxyEnabled,
|
||||
config.MetaProxyRetry,
|
||||
metaProxyClient,
|
||||
)
|
||||
}
|
||||
func DoCrawlerProxyRequest(req *http.Request) (*http.Response, error) {
|
||||
return DoProxyRequest(
|
||||
req,
|
||||
config.CrawlerProxyStrict,
|
||||
config.CrawlerProxyEnabled,
|
||||
config.CrawlerProxyRetry,
|
||||
metaProxyClient,
|
||||
)
|
||||
}
|
||||
|
||||
// tryRequestWithRetry tries the request up to "retries" times, waiting 200ms between attempts.
|
||||
func tryRequestWithRetry(req *http.Request, client Doer, retries int) (*http.Response, error) {
|
||||
var resp *http.Response
|
||||
var err error
|
||||
for i := 1; i <= retries; i++ {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
printDebug("Attempt %d of %d with proxy/client...", i, retries)
|
||||
resp, err = tryRequestOnce(req, client)
|
||||
if isSuccessful(resp, err) {
|
||||
return resp, nil
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// tryRequestOnce sends a single request with the given client. If client is nil, uses default client.
|
||||
func tryRequestOnce(req *http.Request, client Doer) (*http.Response, error) {
|
||||
if client == nil {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// isSuccessful checks if err==nil & resp != nil & resp.StatusCode in [200..299].
|
||||
func isSuccessful(resp *http.Response, err error) bool {
|
||||
if err != nil || resp == nil {
|
||||
return false
|
||||
}
|
||||
return resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
}
|
||||
|
||||
// func main() {
|
||||
// config := loadConfig()
|
||||
|
||||
// // Initialize proxies if enabled
|
||||
// if config.CrawlerProxyEnabled || config.MetaProxyEnabled {
|
||||
// InitProxies()
|
||||
// }
|
||||
|
||||
// // Example usage
|
||||
// if metaProxyClient != nil {
|
||||
// req, _ := http.NewRequest("GET", "https://example.com", nil)
|
||||
// resp, err := metaProxyClient.Do(req)
|
||||
// if err != nil {
|
||||
// fmt.Printf("Error using MetaProxyClient: %v\n", err)
|
||||
// } else {
|
||||
// fmt.Printf("Meta Proxy Response Status: %s\n", resp.Status)
|
||||
// resp.Body.Close()
|
||||
// }
|
||||
// }
|
||||
// }
|
66
run.bat
66
run.bat
|
@ -5,7 +5,7 @@ rem Initialize variables
|
|||
set SKIP_CONFIG=""
|
||||
set PORT=""
|
||||
set DOMAIN=""
|
||||
set CONFIG_FILE=""
|
||||
set BUILD_MODE=false
|
||||
set BUILD_OUTPUT=qgato.exe
|
||||
|
||||
rem Parse arguments
|
||||
|
@ -23,14 +23,13 @@ if "%~1"=="--domain" (
|
|||
shift
|
||||
goto parse_args
|
||||
)
|
||||
if "%~1"=="--config" (
|
||||
set CONFIG_FILE=%~2
|
||||
shift
|
||||
if "%~1"=="--skip-config-check" (
|
||||
set SKIP_CONFIG=--skip-config-check
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
if "%~1"=="--skip-config-check" (
|
||||
set SKIP_CONFIG=--skip-config-check
|
||||
if "%~1"=="--build" (
|
||||
set BUILD_MODE=true
|
||||
shift
|
||||
goto parse_args
|
||||
)
|
||||
|
@ -42,29 +41,46 @@ exit /b 1
|
|||
rem Use the current directory where the script is executed
|
||||
pushd %~dp0
|
||||
|
||||
rem Always delete and rebuild the binary
|
||||
echo Cleaning previous build...
|
||||
if exist "%BUILD_OUTPUT%" del "%BUILD_OUTPUT%"
|
||||
|
||||
echo Building application...
|
||||
go build -ldflags="-s -w" -o "%BUILD_OUTPUT%" .
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
rem Collect all .go files in the current directory excluding *_test.go
|
||||
set GO_FILES=
|
||||
for %%f in (*.go) do (
|
||||
echo %%f | findstr "_test.go" >nul
|
||||
if errorlevel 1 (
|
||||
set GO_FILES=!GO_FILES! %%f
|
||||
)
|
||||
)
|
||||
echo Build successful! Output: %CD%\%BUILD_OUTPUT%
|
||||
|
||||
rem Construct the command
|
||||
set CMD=%BUILD_OUTPUT% !SKIP_CONFIG!
|
||||
if not "%PORT%"=="" set CMD=!CMD! --port %PORT%
|
||||
if not "%DOMAIN%"=="" set CMD=!CMD! --domain %DOMAIN%
|
||||
if not "%CONFIG_FILE%"=="" set CMD=!CMD! --config %CONFIG_FILE%
|
||||
if "%BUILD_MODE%"=="true" (
|
||||
rem Build mode
|
||||
echo Building application...
|
||||
go build -o "%BUILD_OUTPUT%" !GO_FILES!
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
echo Build successful! Output: %CD%\%BUILD_OUTPUT%
|
||||
) else (
|
||||
rem Check if the executable exists
|
||||
if not exist "%BUILD_OUTPUT%" (
|
||||
echo Executable not found. Building it first...
|
||||
go build -o "%BUILD_OUTPUT%" !GO_FILES!
|
||||
if errorlevel 1 (
|
||||
echo Build failed! Unable to run the application.
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
rem Informative output
|
||||
echo Starting application with command: !CMD!
|
||||
rem Construct the command
|
||||
set CMD="%BUILD_OUTPUT% !SKIP_CONFIG!"
|
||||
if not "%PORT%"=="" set CMD=!CMD! --port %PORT%
|
||||
if not "%DOMAIN%"=="" set CMD=!CMD! --domain %DOMAIN%
|
||||
|
||||
rem Run the built executable
|
||||
call !CMD!
|
||||
rem Informative output
|
||||
echo Starting application with command: !CMD!
|
||||
|
||||
rem Run the application
|
||||
call !CMD!
|
||||
)
|
||||
|
||||
rem Return to the original directory
|
||||
popd
|
||||
|
|
86
run.sh
86
run.sh
|
@ -4,9 +4,7 @@
|
|||
SKIP_CONFIG=""
|
||||
PORT=""
|
||||
DOMAIN=""
|
||||
CONFIG_FILE=""
|
||||
BUILD_ONLY=0
|
||||
PLATFORM="linux"
|
||||
BUILD_MODE=false
|
||||
BUILD_OUTPUT="qgato"
|
||||
|
||||
# Parse arguments
|
||||
|
@ -20,22 +18,14 @@ while [ $# -gt 0 ]; do
|
|||
DOMAIN=$2
|
||||
shift 2
|
||||
;;
|
||||
--config)
|
||||
CONFIG_FILE=$2
|
||||
shift 2
|
||||
;;
|
||||
--platform)
|
||||
PLATFORM=$2
|
||||
shift 2
|
||||
;;
|
||||
--build-only)
|
||||
BUILD_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--skip-config-check)
|
||||
SKIP_CONFIG="--skip-config-check"
|
||||
shift
|
||||
;;
|
||||
--build)
|
||||
BUILD_MODE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1"
|
||||
exit 1
|
||||
|
@ -46,40 +36,36 @@ done
|
|||
# Get the directory of the script
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
|
||||
# Set GOOS and output filename
|
||||
if [ "$PLATFORM" = "windows" ]; then
|
||||
GOOS=windows
|
||||
BUILD_OUTPUT="qgato.exe"
|
||||
# List all Go files in the script directory (excluding test files)
|
||||
GO_FILES=$(find "$SCRIPT_DIR" -name '*.go' ! -name '*_test.go' -print)
|
||||
|
||||
if $BUILD_MODE; then
|
||||
# Build mode
|
||||
echo "Building application..."
|
||||
go build -o "$SCRIPT_DIR/$BUILD_OUTPUT" $GO_FILES
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Build successful! Output: $SCRIPT_DIR/$BUILD_OUTPUT"
|
||||
else
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
GOOS=linux
|
||||
BUILD_OUTPUT="qgato"
|
||||
# Run mode
|
||||
CMD="./$BUILD_OUTPUT $SKIP_CONFIG"
|
||||
[ -n "$PORT" ] && CMD="$CMD --port $PORT"
|
||||
[ -n "$DOMAIN" ] && CMD="$CMD --domain $DOMAIN"
|
||||
|
||||
if [ ! -f "$SCRIPT_DIR/$BUILD_OUTPUT" ]; then
|
||||
echo "Executable not found. Building it first..."
|
||||
go build -o "$SCRIPT_DIR/$BUILD_OUTPUT" $GO_FILES
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed! Unable to run the application."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Starting application with command: $CMD"
|
||||
|
||||
# Run the executable
|
||||
eval $CMD
|
||||
fi
|
||||
|
||||
# Clean and build
|
||||
echo "Cleaning previous build..."
|
||||
rm -f "$SCRIPT_DIR/$BUILD_OUTPUT"
|
||||
|
||||
echo "Building application for $PLATFORM..."
|
||||
GOOS=$GOOS go build -ldflags="-s -w" -o "$SCRIPT_DIR/$BUILD_OUTPUT" .
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Build successful! Output: $SCRIPT_DIR/$BUILD_OUTPUT"
|
||||
else
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Skip execution if build-only
|
||||
if [ "$BUILD_ONLY" -eq 1 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Construct the run command
|
||||
CMD="$SCRIPT_DIR/$BUILD_OUTPUT $SKIP_CONFIG"
|
||||
[ -n "$PORT" ] && CMD="$CMD --port $PORT"
|
||||
[ -n "$DOMAIN" ] && CMD="$CMD --domain $DOMAIN"
|
||||
[ -n "$CONFIG_FILE" ] && CMD="$CMD --config $CONFIG_FILE"
|
||||
|
||||
echo "Starting application with command: $CMD"
|
||||
|
||||
# Run the built executable
|
||||
eval $CMD
|
||||
|
|
|
@ -31,5 +31,5 @@
|
|||
font-family: 'Material Icons Round';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/static/fonts/MaterialIcons-Round.woff2') format('woff2');
|
||||
src: url('/static/fonts/material-icons-round-v108-latin-regular.woff2') format('woff2');
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/* Image Loading Effect */
|
||||
.loading-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background-color: var(--snip-background);
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 25%,
|
||||
rgba(255, 255, 255, 0.15) 50%,
|
||||
rgba(255, 255, 255, 0) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: image-wave 2s infinite linear;
|
||||
}
|
||||
|
||||
/* Title Loading Effect */
|
||||
.title-loading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
color: transparent !important;
|
||||
background-color: var(--snip-background);
|
||||
min-height: 1.2em;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
top: 2px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.title-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 25%,
|
||||
rgba(255, 255, 255, 0.25) 50%,
|
||||
transparent 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: title-wave 2.5s infinite linear;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes image-wave {
|
||||
0% {
|
||||
background-position: -100% 0; /* Start off-screen left */
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 0; /* End off-screen right */
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes title-wave {
|
||||
0% {
|
||||
background-position: -100% 0; /* Start off-screen left */
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 0; /* End off-screen right */
|
||||
}
|
||||
}
|
|
@ -33,7 +33,6 @@
|
|||
#viewer-image {
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Viewer Title */
|
||||
|
@ -102,13 +101,13 @@
|
|||
|
||||
/* View Image Container */
|
||||
#viewer-image-container {
|
||||
background-color: #0000;
|
||||
background-color: var(--view-image-color);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
/* Full Size and Proxy Size Links */
|
||||
|
@ -154,24 +153,14 @@
|
|||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media only screen and (max-width: 880px) {
|
||||
@media only screen and (max-width: 750px) {
|
||||
#image-viewer {
|
||||
width: 100%;
|
||||
height: 100% !important;
|
||||
margin-top: 28px;
|
||||
height: 77%;
|
||||
margin-top: -33px;
|
||||
margin-right: 0%;
|
||||
border-top-right-radius: 0px;
|
||||
border-top-left-radius: 0px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.material-icons-round {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
#viewer-image-container {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#viewer-image {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
.favicon-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.favicon-wrapper.loading img {
|
||||
visibility: hidden; /* hide placeholder */
|
||||
}
|
||||
|
||||
.favicon-wrapper.loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: -8px 0 0 -8px;
|
||||
border: 2px solid var(--html-bg);
|
||||
border-top-color: var(--fg);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.7s linear infinite;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
.message-bottom-right {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: var(--search-bg);
|
||||
color: var(--text-color);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 0 10px var(--box-shadow);
|
||||
}
|
||||
|
||||
.message-bottom-right.visible {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
30% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
70% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
85% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
95% {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.dot {
|
||||
display: inline-block;
|
||||
animation: bounce 1.5s infinite;
|
||||
}
|
||||
|
||||
.dot:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.dot:nth-child(3) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.dot:nth-child(4) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
/* Music Results Styling */
|
||||
.result-item.music-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.music-thumbnail {
|
||||
position: relative;
|
||||
flex: 0 0 160px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
background: var(--placeholder-bg);
|
||||
}
|
||||
|
||||
.music-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.music-thumbnail:hover img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
|
||||
.thumbnail-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--placeholder-bg);
|
||||
color: var(--placeholder-icon);
|
||||
}
|
||||
|
||||
.thumbnail-placeholder .material-icons-round {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.duration-overlay {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.music-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.music-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.music-title:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.music-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.artist {
|
||||
color: var(--accent-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.meta-separator {
|
||||
color: var(--border-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.music-thumbnail {
|
||||
flex-basis: 120px;
|
||||
}
|
||||
|
||||
.music-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.music-meta {
|
||||
font-size: 13px;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.music-thumbnail {
|
||||
flex-basis: 100px;
|
||||
}
|
||||
|
||||
.duration-overlay {
|
||||
font-size: 11px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
}
|
|
@ -60,17 +60,8 @@
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
.fetched_dif_videos {
|
||||
margin-top: 110px !important;
|
||||
}
|
||||
|
||||
.fetched_dif_files{
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
|
||||
.fetched_dif_images {
|
||||
margin-top: 10px ;
|
||||
.fetched_dif {
|
||||
margin-top: 110px !important;
|
||||
}
|
||||
|
||||
.fetched_img {
|
||||
|
@ -303,7 +294,6 @@ html {
|
|||
}
|
||||
|
||||
.btn-nostyle {
|
||||
font-family: 'Inter', Arial, Helvetica, sans-serif !important;
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
padding: 0px;
|
||||
|
@ -384,11 +374,10 @@ hr {
|
|||
.results .video_title {
|
||||
font-size: 16px;
|
||||
}
|
||||
/*
|
||||
this is so stupid, separate css into general style and per result page css style to avoid this
|
||||
.video_title h3 {
|
||||
margin-top: 0px !important;
|
||||
} */
|
||||
|
||||
.video_title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.video_title a {
|
||||
color: var(--link);
|
||||
|
@ -408,7 +397,6 @@ this is so stupid, separate css into general style and per result page css style
|
|||
width: 254px;
|
||||
height: 143px;
|
||||
object-fit: cover;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.video__img__results {
|
||||
|
@ -439,19 +427,13 @@ this is so stupid, separate css into general style and per result page css style
|
|||
.duration {
|
||||
position: absolute;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
padding: .5em;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
background: rgba(0, 0, 0, .5);
|
||||
right: 0;
|
||||
margin-top: -28px !important;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.4px;
|
||||
bottom: 6px;
|
||||
right: 2px;
|
||||
border-radius: 3px;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.pipe {
|
||||
|
@ -641,10 +623,6 @@ this is so stupid, separate css into general style and per result page css style
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
.torrent-cat {
|
||||
margin-top: 110px;
|
||||
}
|
||||
|
||||
.torrent-cat:hover,
|
||||
.torrent-settings:hover,
|
||||
.torrent-sort-save:hover {
|
||||
|
@ -1183,7 +1161,8 @@ p {
|
|||
color: var(--fg);
|
||||
width: 530px;
|
||||
padding: 15px;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 627px;
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.58;
|
||||
letter-spacing: normal;
|
||||
|
@ -1309,113 +1288,24 @@ p {
|
|||
text-shadow: 1px 1px 2px var(--border) !important; /* Adjust text shadow */
|
||||
}
|
||||
|
||||
/* Favicon styling */
|
||||
.result_header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
.message-bottom-left {
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background-color: var(--search-bg);
|
||||
color: var(--text-color);
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
z-index: 1000;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 0 10px var(--box-shadow);
|
||||
}
|
||||
|
||||
.favicon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 18px;
|
||||
border-radius: 8%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* Result link styling */
|
||||
.result-link {
|
||||
color: var(--fg);
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Result item spacing */
|
||||
.result_item {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.result-title h3 {
|
||||
margin: 4px 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.single-line-ellipsis {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clamp-3-lines {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
|
||||
/* Standard syntax (future support) */
|
||||
line-clamp: 3;
|
||||
box-orient: vertical;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.5; /* adjust if needed */
|
||||
max-height: calc(1.5em * 3); /* 3 lines */
|
||||
}
|
||||
|
||||
.result-description {
|
||||
margin: 4px 0 0 0;
|
||||
color: var(--font-fg);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.results br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.result-url {
|
||||
font-size: 14px;
|
||||
color: var(--fg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.result-domain {
|
||||
color: var(--fg);
|
||||
font-weight: 600;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.result-path {
|
||||
color: var(--font-fg);
|
||||
font-weight: 500;
|
||||
opacity: 0.8;
|
||||
}
|
||||
/*
|
||||
.result-path::before {
|
||||
content: "›";
|
||||
margin: 0 4px;
|
||||
opacity: 0.6;
|
||||
} */
|
||||
|
||||
body, h1, p, a, input, button {
|
||||
color: var(--text-color); /* Applies the text color based on theme */
|
||||
background-color: var(--background-color); /* Applies the background color based on theme */
|
||||
|
@ -1699,27 +1589,15 @@ body, h1, p, a, input, button {
|
|||
}
|
||||
|
||||
.fetched_img {
|
||||
margin-top: 25px !important;
|
||||
margin-top: 135px !important;
|
||||
margin-left: 1.2% !important;
|
||||
left: 0px !important;
|
||||
}
|
||||
|
||||
.fetched_vid {
|
||||
margin-top: 25px !important;
|
||||
margin-top: 135px !important;
|
||||
}
|
||||
|
||||
.fetched_dif_videos {
|
||||
margin-top: 135px !important;
|
||||
}
|
||||
|
||||
.fetched_dif_files{
|
||||
margin-top: 25px !important;
|
||||
}
|
||||
|
||||
.fetched_dif_images {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.results_settings {
|
||||
left: 20px;
|
||||
font-size: 13px;
|
||||
|
@ -1731,7 +1609,6 @@ body, h1, p, a, input, button {
|
|||
}
|
||||
|
||||
form.torrent-sort {
|
||||
margin-top: 35px;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
|
@ -1850,8 +1727,4 @@ body, h1, p, a, input, button {
|
|||
.leaflet-control-attribution a {
|
||||
color: var(--link) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.favicon.globe-fallback {
|
||||
color: var(--font-fg);
|
||||
}
|
||||
}
|
Binary file not shown.
BIN
static/fonts/material-icons-round-v108-latin-regular.woff2
Normal file
BIN
static/fonts/material-icons-round-v108-latin-regular.woff2
Normal file
Binary file not shown.
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="currentColor">
|
||||
<path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 986 B |
|
@ -8,24 +8,16 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||
let searchType = templateData.getAttribute('data-type') || 'text'; // Default to 'text' if not provided
|
||||
let loading = false;
|
||||
let hasMoreResults = true;
|
||||
const loadingIndicator = document.getElementById('message-bottom-right');
|
||||
const loadingIndicator = document.getElementById('message-bottom-left');
|
||||
let loadingTimeout;
|
||||
|
||||
function showLoadingMessage() {
|
||||
loadingIndicator.classList.add('visible');
|
||||
}
|
||||
|
||||
function hideLoadingMessage() {
|
||||
loadingIndicator.classList.remove('visible');
|
||||
}
|
||||
|
||||
function loadResults(newPage) {
|
||||
if (loading || !hasMoreResults) return;
|
||||
loading = true;
|
||||
|
||||
// Show loading indicator if taking more than 150ms
|
||||
loadingTimeout = setTimeout(() => {
|
||||
showLoadingMessage()
|
||||
loadingIndicator.style.display = 'flex';
|
||||
}, 150);
|
||||
|
||||
fetch(`/search?q=${encodeURIComponent(query)}&t=${encodeURIComponent(searchType)}&p=${newPage}`)
|
||||
|
@ -37,7 +29,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||
})
|
||||
.then(data => {
|
||||
clearTimeout(loadingTimeout);
|
||||
hideLoadingMessage()
|
||||
loadingIndicator.style.display = 'none';
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(data, 'text/html');
|
||||
const newResultsHTML = doc.getElementById('results').innerHTML;
|
||||
|
@ -63,7 +55,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||
})
|
||||
.catch(error => {
|
||||
clearTimeout(loadingTimeout);
|
||||
hideLoadingMessage()
|
||||
loadingIndicator.style.display = 'none';
|
||||
console.error('Error loading results:', error);
|
||||
hasMoreResults = false;
|
||||
loading = false;
|
||||
|
|
|
@ -1,186 +1,197 @@
|
|||
(function() {
|
||||
// Add loading effects to image and title
|
||||
function addLoadingEffects(imgElement) {
|
||||
const container = imgElement.closest('.image');
|
||||
if (!container) return; // avoid null dereference
|
||||
|
||||
const title = imgElement.closest('.image').querySelector('.img_title');
|
||||
imgElement.classList.add('loading-image');
|
||||
title.classList.add('title-loading');
|
||||
}
|
||||
|
||||
function removeLoadingEffects(imgElement) {
|
||||
const title = imgElement.closest('.image').querySelector('.img_title');
|
||||
imgElement.classList.remove('loading-image');
|
||||
title.classList.remove('title-loading');
|
||||
|
||||
if (imgElement.src.endsWith('/images/missing.svg')) {
|
||||
imgElement.closest('.image').remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Modified handleImageError with theme-consistent error handling
|
||||
function handleImageError(imgElement, retryCount = 3, retryDelay = 1000) {
|
||||
const container = imgElement.closest('.image');
|
||||
const title = container.querySelector('.img_title');
|
||||
|
||||
if (retryCount > 0) {
|
||||
setTimeout(() => {
|
||||
imgElement.src = imgElement.getAttribute('data-full');
|
||||
imgElement.onerror = () => handleImageError(imgElement, retryCount - 1, retryDelay);
|
||||
}, retryDelay);
|
||||
} else {
|
||||
imgElement.classList.remove('loading-image');
|
||||
title.classList.remove('title-loading');
|
||||
container.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const imageStatusInterval = 500;
|
||||
const scrollThreshold = 500;
|
||||
const loadingIndicator = document.getElementById('message-bottom-right'); let loadingTimer;
|
||||
// Configuration
|
||||
const imageStatusInterval = 500; // Interval in milliseconds to check image status
|
||||
const scrollThreshold = 500; // Distance from bottom of the page to trigger loading
|
||||
const loadingIndicator = document.getElementById('message-bottom-left');
|
||||
let loadingTimer;
|
||||
let isFetching = false;
|
||||
let page = parseInt(document.getElementById('template-data').getAttribute('data-page')) || 1;
|
||||
let query = document.getElementById('template-data').getAttribute('data-query');
|
||||
let hardCacheEnabled = document.getElementById('template-data').getAttribute('data-hard-cache-enabled') === 'true';
|
||||
let noMoreImages = false;
|
||||
let noMoreImages = false; // Flag to indicate if there are no more images to load
|
||||
|
||||
let imageElements = [];
|
||||
let imageIds = [];
|
||||
let imageStatusTimer;
|
||||
|
||||
function showLoadingMessage() {
|
||||
loadingIndicator.classList.add('visible');
|
||||
}
|
||||
|
||||
function hideLoadingMessage() {
|
||||
loadingIndicator.classList.remove('visible');
|
||||
/**
|
||||
* Function to handle image load errors with retry logic
|
||||
* @param {HTMLElement} imgElement - The image element that failed to load
|
||||
* @param {number} retryCount - Number of retries left
|
||||
* @param {number} retryDelay - Delay between retries in milliseconds
|
||||
*/
|
||||
function handleImageError(imgElement, retryCount = 3, retryDelay = 1000) {
|
||||
if (retryCount > 0) {
|
||||
setTimeout(() => {
|
||||
imgElement.src = imgElement.getAttribute('data-full');
|
||||
imgElement.onerror = function() {
|
||||
handleImageError(imgElement, retryCount - 1, retryDelay);
|
||||
};
|
||||
}, retryDelay);
|
||||
} else {
|
||||
// After retries, hide the image container or set a fallback image
|
||||
console.warn('Image failed to load after retries:', imgElement.getAttribute('data-full'));
|
||||
imgElement.parentElement.style.display = 'none'; // Hide the image container
|
||||
// Alternatively, set a fallback image:
|
||||
// imgElement.src = '/static/images/fallback.svg';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to ensure the page is scrollable by loading more images if necessary
|
||||
*/
|
||||
function ensureScrollable() {
|
||||
if (noMoreImages) return;
|
||||
if (noMoreImages) return; // Do not attempt if no more images are available
|
||||
// Check if the page is not scrollable
|
||||
if (document.body.scrollHeight <= window.innerHeight) {
|
||||
// If not scrollable, fetch the next page
|
||||
fetchNextPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to fetch the next page of images
|
||||
*/
|
||||
function fetchNextPage() {
|
||||
if (isFetching || noMoreImages) return;
|
||||
|
||||
// Start the timer for loading indicator
|
||||
loadingTimer = setTimeout(() => {
|
||||
showLoadingMessage();
|
||||
loadingIndicator.style.display = 'flex';
|
||||
}, 150);
|
||||
|
||||
isFetching = true;
|
||||
page += 1;
|
||||
|
||||
|
||||
fetch(`/search?q=${encodeURIComponent(query)}&t=image&p=${page}&ajax=true`)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
clearTimeout(loadingTimer);
|
||||
hideLoadingMessage();
|
||||
|
||||
let tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
let newImages = tempDiv.querySelectorAll('.image');
|
||||
|
||||
clearTimeout(loadingTimer); // Clear the timer if fetch is successful
|
||||
loadingIndicator.style.display = 'none'; // Hide the loading indicator
|
||||
|
||||
let parser = new DOMParser();
|
||||
let doc = parser.parseFromString(html, 'text/html');
|
||||
let newImages = doc.querySelectorAll('.image');
|
||||
|
||||
if (newImages.length > 0) {
|
||||
let resultsContainer = document.querySelector('.images');
|
||||
newImages.forEach(imageDiv => {
|
||||
let clonedImageDiv = imageDiv.cloneNode(true);
|
||||
resultsContainer.appendChild(clonedImageDiv);
|
||||
// Append new images to the container
|
||||
resultsContainer.appendChild(imageDiv);
|
||||
|
||||
let img = clonedImageDiv.querySelector('img');
|
||||
if (img && img.getAttribute('data-id')) {
|
||||
addLoadingEffects(img);
|
||||
if (hardCacheEnabled) {
|
||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
img.onerror = () => handleImageError(img);
|
||||
// Get the img element
|
||||
let img = imageDiv.querySelector('img');
|
||||
if (img) {
|
||||
let id = img.getAttribute('data-id');
|
||||
if (id) {
|
||||
imageElements.push(img);
|
||||
imageIds.push(img.getAttribute('data-id'));
|
||||
imageIds.push(id);
|
||||
}
|
||||
if (hardCacheEnabled) {
|
||||
// Replace image with placeholder
|
||||
img.src = '/static/images/placeholder.svg';
|
||||
img.onerror = function() {
|
||||
handleImageError(img);
|
||||
};
|
||||
} else {
|
||||
// HardCacheEnabled is false; load images immediately
|
||||
img.src = img.getAttribute('data-full');
|
||||
img.onload = () => removeLoadingEffects(img);
|
||||
img.onerror = () => handleImageError(img);
|
||||
img.onerror = function() {
|
||||
handleImageError(img);
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hardCacheEnabled) checkImageStatus();
|
||||
if (hardCacheEnabled) {
|
||||
checkImageStatus();
|
||||
}
|
||||
// After appending new images, ensure the page is scrollable
|
||||
ensureScrollable();
|
||||
} else {
|
||||
// No more images to load
|
||||
noMoreImages = true;
|
||||
}
|
||||
isFetching = false;
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(loadingTimer);
|
||||
hideLoadingMessage();
|
||||
console.error('Fetch error:', error);
|
||||
clearTimeout(loadingTimer); // Clear the timer if fetch fails
|
||||
loadingIndicator.style.display = 'none'; // Hide the loading indicator
|
||||
console.error('Error fetching next page:', error);
|
||||
isFetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to check image status via AJAX
|
||||
*/
|
||||
function checkImageStatus() {
|
||||
if (!hardCacheEnabled || imageIds.length === 0) return;
|
||||
if (!hardCacheEnabled) return;
|
||||
if (imageIds.length === 0) {
|
||||
// No images to check, do nothing
|
||||
return;
|
||||
}
|
||||
|
||||
// Send AJAX request to check image status
|
||||
fetch(`/image_status?image_ids=${imageIds.join(',')}`)
|
||||
.then(response => response.json())
|
||||
.then(statusMap => {
|
||||
const pendingImages = [];
|
||||
const pendingIds = [];
|
||||
|
||||
imageElements.forEach(img => {
|
||||
const id = img.getAttribute('data-id');
|
||||
imageElements = imageElements.filter(img => {
|
||||
let id = img.getAttribute('data-id');
|
||||
if (statusMap[id]) {
|
||||
// Image is ready, update src
|
||||
img.src = statusMap[id];
|
||||
img.onload = () => removeLoadingEffects(img);
|
||||
img.onerror = () => handleImageError(img);
|
||||
} else {
|
||||
pendingImages.push(img);
|
||||
pendingIds.push(id);
|
||||
img.onerror = function() {
|
||||
handleImageError(img);
|
||||
};
|
||||
// Remove the image id from the list
|
||||
imageIds = imageIds.filter(imageId => imageId !== id);
|
||||
return false; // Remove img from imageElements
|
||||
}
|
||||
return true; // Keep img in imageElements
|
||||
});
|
||||
|
||||
imageElements = pendingImages;
|
||||
imageIds = pendingIds;
|
||||
// After updating images, ensure the page is scrollable
|
||||
ensureScrollable();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Status check error:', error);
|
||||
console.error('Error checking image status:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize with loading effects
|
||||
document.querySelectorAll('img[data-id]').forEach(img => {
|
||||
const id = img.getAttribute('data-id');
|
||||
if (id) {
|
||||
addLoadingEffects(img);
|
||||
imageElements.push(img);
|
||||
imageIds.push(id);
|
||||
if (hardCacheEnabled) {
|
||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
} else {
|
||||
img.src = img.getAttribute('data-full');
|
||||
img.onload = () => removeLoadingEffects(img);
|
||||
}
|
||||
img.onerror = () => handleImageError(img);
|
||||
}
|
||||
});
|
||||
// Initialize imageElements and imageIds
|
||||
imageElements = Array.from(document.querySelectorAll('img[data-id]'));
|
||||
imageIds = imageElements
|
||||
.map(img => img.getAttribute('data-id'))
|
||||
.filter(id => id); // Exclude empty IDs
|
||||
|
||||
// Rest of your existing code remains unchanged
|
||||
if (hardCacheEnabled) {
|
||||
imageStatusTimer = setInterval(checkImageStatus, imageStatusInterval);
|
||||
checkImageStatus();
|
||||
// Replace images with placeholders
|
||||
imageElements.forEach(img => {
|
||||
img.src = '/static/images/placeholder.svg';
|
||||
});
|
||||
|
||||
// Start checking image status
|
||||
let imageStatusTimer = setInterval(checkImageStatus, imageStatusInterval);
|
||||
checkImageStatus(); // Initial check
|
||||
} else {
|
||||
// HardCacheEnabled is false; load images immediately
|
||||
imageElements.forEach(img => {
|
||||
img.src = img.getAttribute('data-full');
|
||||
img.onerror = function() {
|
||||
handleImageError(img);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// After initial images are loaded, ensure the page is scrollable
|
||||
window.addEventListener('load', ensureScrollable);
|
||||
window.addEventListener('scroll', () => {
|
||||
|
||||
// Infinite scrolling
|
||||
window.addEventListener('scroll', function() {
|
||||
if (isFetching || noMoreImages) return;
|
||||
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - scrollThreshold) {
|
||||
// User scrolled near the bottom
|
||||
fetchNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (imageStatusTimer) clearInterval(imageStatusTimer);
|
||||
});
|
||||
})();
|
|
@ -1,277 +0,0 @@
|
|||
(function() {
|
||||
// Get template data and configuration
|
||||
const templateData = document.getElementById('template-data');
|
||||
const type = templateData.getAttribute('data-type');
|
||||
const hardCacheEnabled = templateData.getAttribute('data-hard-cache-enabled') === 'true';
|
||||
|
||||
// Track all favicon/image elements and their IDs
|
||||
let allMediaElements = [];
|
||||
let allMediaIds = [];
|
||||
const mediaMap = new Map();
|
||||
|
||||
// Add loading effects to image/favicon and associated text
|
||||
function addLoadingEffects(imgElement) {
|
||||
const container = imgElement.closest(type === 'image' ? '.image' : '.result_item');
|
||||
if (!container) return;
|
||||
|
||||
const titleSelector = type === 'image' ? '.img_title' : '.result-url';
|
||||
const title = container.querySelector(titleSelector);
|
||||
imgElement.closest('.favicon-wrapper')?.classList.add('loading');
|
||||
// if (title) title.classList.add('title-loading');
|
||||
}
|
||||
|
||||
// Remove loading effects when image/favicon loads
|
||||
function removeLoadingEffects(imgElement) {
|
||||
const container = imgElement.closest(type === 'image' ? '.image' : '.result_item');
|
||||
const titleSelector = type === 'image' ? '.img_title' : '.result-url';
|
||||
const title = container?.querySelector(titleSelector);
|
||||
imgElement.closest('.favicon-wrapper')?.classList.remove('loading');
|
||||
if (title) title.classList.remove('title-loading');
|
||||
|
||||
if (type === 'image' && imgElement.src.endsWith('/images/globe.svg')) {
|
||||
container.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle image/favicon loading errors
|
||||
function handleImageError(imgElement, retryCount = 8, retryDelay = 500) {
|
||||
const isFavicon = !!imgElement.closest('.favicon-wrapper');
|
||||
const container = imgElement.closest(type === 'image' ? '.image' : '.result_item');
|
||||
const titleSelector = type === 'image' ? '.img_title' : '.result-url';
|
||||
const title = container?.querySelector(titleSelector);
|
||||
const fullURL = imgElement.getAttribute('data-full');
|
||||
|
||||
if (retryCount > 0 && !imgElement.dataset.checked404) {
|
||||
imgElement.dataset.checked404 = '1'; // avoid infinite loop
|
||||
|
||||
fetch(fullURL, { method: 'HEAD' })
|
||||
.then(res => {
|
||||
if (res.status === 404) {
|
||||
fallbackToGlobe(imgElement);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
imgElement.src = fullURL;
|
||||
imgElement.onerror = () => handleImageError(imgElement, retryCount - 1, retryDelay);
|
||||
}, retryDelay);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
fallbackToGlobe(imgElement);
|
||||
});
|
||||
} else {
|
||||
fallbackToGlobe(imgElement);
|
||||
}
|
||||
|
||||
function fallbackToGlobe(imgElement) {
|
||||
imgElement.closest('.favicon-wrapper')?.classList.remove('loading');
|
||||
if (title) title.classList.remove('title-loading');
|
||||
|
||||
if (isFavicon) {
|
||||
const wrapper = imgElement.closest('.favicon-wrapper') || imgElement.parentElement;
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
svg.setAttribute("viewBox", "0 -960 960 960");
|
||||
svg.setAttribute("height", imgElement.height || "16");
|
||||
svg.setAttribute("width", imgElement.width || "16");
|
||||
svg.setAttribute("fill", "currentColor");
|
||||
svg.classList.add("favicon", "globe-fallback");
|
||||
svg.innerHTML = `<path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/>`;
|
||||
imgElement.remove();
|
||||
wrapper.appendChild(svg);
|
||||
} else if (type === 'image') {
|
||||
container?.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shared configuration
|
||||
const statusCheckInterval = 500;
|
||||
const scrollThreshold = 500;
|
||||
const loadingIndicator = document.getElementById('message-bottom-right');
|
||||
let loadingTimer;
|
||||
let isFetching = false;
|
||||
let page = parseInt(templateData.getAttribute('data-page')) || 1;
|
||||
let query = templateData.getAttribute('data-query');
|
||||
let noMoreImages = false;
|
||||
|
||||
function showLoadingMessage() {
|
||||
loadingIndicator.classList.add('visible');
|
||||
}
|
||||
|
||||
function hideLoadingMessage() {
|
||||
loadingIndicator.classList.remove('visible');
|
||||
}
|
||||
|
||||
function ensureScrollable() {
|
||||
if (noMoreImages) return;
|
||||
if (document.body.scrollHeight <= window.innerHeight) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}
|
||||
|
||||
// Register a new media element for tracking
|
||||
function registerMediaElement(imgElement) {
|
||||
const id = imgElement.getAttribute('data-id');
|
||||
if (!id) return;
|
||||
|
||||
let wrapper = imgElement.closest('.favicon-wrapper');
|
||||
if (!wrapper) {
|
||||
wrapper = document.createElement('span');
|
||||
wrapper.classList.add('favicon-wrapper');
|
||||
imgElement.replaceWith(wrapper);
|
||||
wrapper.appendChild(imgElement);
|
||||
}
|
||||
|
||||
addLoadingEffects(imgElement);
|
||||
|
||||
if (hardCacheEnabled) {
|
||||
imgElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
|
||||
imgElement.onerror = () => handleImageError(imgElement, 3, 1000);
|
||||
} else {
|
||||
imgElement.src = imgElement.getAttribute('data-full');
|
||||
imgElement.onload = () => removeLoadingEffects(imgElement);
|
||||
imgElement.onerror = () => handleImageError(imgElement, 3, 1000);
|
||||
}
|
||||
|
||||
// Track it
|
||||
if (!mediaMap.has(id)) {
|
||||
mediaMap.set(id, []);
|
||||
}
|
||||
mediaMap.get(id).push(imgElement);
|
||||
}
|
||||
|
||||
// Check status of all tracked media elements
|
||||
function checkMediaStatus() {
|
||||
const allIds = Array.from(mediaMap.keys());
|
||||
if (allIds.length === 0) return;
|
||||
|
||||
const idGroups = [];
|
||||
for (let i = 0; i < allIds.length; i += 50) {
|
||||
idGroups.push(allIds.slice(i, i + 50));
|
||||
}
|
||||
|
||||
const processGroups = async () => {
|
||||
const stillPending = new Map();
|
||||
|
||||
for (const group of idGroups) {
|
||||
try {
|
||||
const response = await fetch(`/image_status?image_ids=${group.join(',')}`);
|
||||
const statusMap = await response.json();
|
||||
|
||||
group.forEach(id => {
|
||||
const elements = mediaMap.get(id);
|
||||
const resolved = statusMap[id];
|
||||
if (!elements) return;
|
||||
if (resolved && resolved !== 'pending') {
|
||||
elements.forEach(img => {
|
||||
img.src = resolved;
|
||||
img.onload = () => removeLoadingEffects(img);
|
||||
img.onerror = () => handleImageError(img);
|
||||
});
|
||||
} else {
|
||||
stillPending.set(id, elements);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Status check failed:', err);
|
||||
group.forEach(id => {
|
||||
if (mediaMap.has(id)) {
|
||||
stillPending.set(id, mediaMap.get(id));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mediaMap.clear();
|
||||
for (const [id, imgs] of stillPending) {
|
||||
mediaMap.set(id, imgs);
|
||||
}
|
||||
};
|
||||
|
||||
processGroups();
|
||||
}
|
||||
|
||||
function fetchNextPage() {
|
||||
if (isFetching || noMoreImages) return;
|
||||
|
||||
loadingTimer = setTimeout(() => {
|
||||
showLoadingMessage();
|
||||
}, 150);
|
||||
|
||||
isFetching = true;
|
||||
page += 1;
|
||||
|
||||
fetch(`/search?q=${encodeURIComponent(query)}&t=${type}&p=${page}&ajax=true`)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
clearTimeout(loadingTimer);
|
||||
hideLoadingMessage();
|
||||
|
||||
let tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = html;
|
||||
let newItems = tempDiv.querySelectorAll(type === 'image' ? '.image' : '.result_item');
|
||||
|
||||
if (newItems.length > 0) {
|
||||
let resultsContainer = document.querySelector(type === 'image' ? '.images' : '.results');
|
||||
newItems.forEach(item => {
|
||||
let clonedItem = item.cloneNode(true);
|
||||
resultsContainer.appendChild(clonedItem);
|
||||
|
||||
// Register any new media elements
|
||||
const img = clonedItem.querySelector('img[data-id]');
|
||||
if (img) {
|
||||
registerMediaElement(img);
|
||||
}
|
||||
});
|
||||
|
||||
ensureScrollable();
|
||||
} else {
|
||||
noMoreImages = true;
|
||||
}
|
||||
isFetching = false;
|
||||
})
|
||||
.catch(error => {
|
||||
clearTimeout(loadingTimer);
|
||||
hideLoadingMessage();
|
||||
console.error('Fetch error:', error);
|
||||
isFetching = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize all existing media elements
|
||||
function initializeMediaElements() {
|
||||
document.querySelectorAll('img[data-id]').forEach(img => {
|
||||
registerMediaElement(img);
|
||||
});
|
||||
}
|
||||
|
||||
function startStatusPolling() {
|
||||
checkMediaStatus();
|
||||
setInterval(checkMediaStatus, statusCheckInterval);
|
||||
}
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
initializeMediaElements();
|
||||
if (hardCacheEnabled) startStatusPolling();
|
||||
} else {
|
||||
window.addEventListener('load', () => {
|
||||
initializeMediaElements();
|
||||
if (hardCacheEnabled) startStatusPolling();
|
||||
});
|
||||
}
|
||||
|
||||
// Infinite scroll handler
|
||||
window.addEventListener('scroll', () => {
|
||||
if (isFetching || noMoreImages) return;
|
||||
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - scrollThreshold) {
|
||||
fetchNextPage();
|
||||
}
|
||||
});
|
||||
|
||||
// // Clean up on page unload
|
||||
// window.addEventListener('beforeunload', () => {
|
||||
// if (statusCheckTimeout) {
|
||||
// clearTimeout(statusCheckTimeout);
|
||||
// }
|
||||
// });
|
||||
})();
|
|
@ -28,41 +28,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
<img id="viewer-image" class="view-image-img" src="" alt="">
|
||||
</div>
|
||||
<p class="image-alt" id="viewer-title"></p>
|
||||
<br>
|
||||
<div class="search-type-icons" style="display:flex; justify-content:center; flex-wrap: wrap;">
|
||||
<div class="icon-button">
|
||||
<button class="material-icons-round clickable btn-nostyle" id="viewer-copy-link">
|
||||
<span class="material-icons-round"></span>
|
||||
<p>Copy link</p>
|
||||
</button>
|
||||
</div>
|
||||
<div class="icon-button">
|
||||
<button class="material-icons-round clickable btn-nostyle" id="viewer-open-image">
|
||||
<span class="material-icons-round"></span>
|
||||
<p>Open image</p>
|
||||
</button>
|
||||
</div>
|
||||
<div class="icon-button">
|
||||
<button class="material-icons-round clickable btn-nostyle" id="viewer-open-source">
|
||||
<span class="material-icons-round"></span>
|
||||
<p>Go to source</p>
|
||||
</button>
|
||||
</div>
|
||||
<div class="icon-button">
|
||||
<button class="material-icons-round clickable btn-nostyle" id="viewer-download-image">
|
||||
<span class="material-icons-round"></span>
|
||||
<p>Download</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<a class="full-size" id="viewer-full-size-link" href="#" target="_blank">Show source website</a>
|
||||
<a class="proxy-size" id="viewer-proxy-size-link" href="#" target="_blank">Show in fullscreen</a>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const imageView = viewerOverlay.querySelector('#image-viewer');
|
||||
if (!imageView) {
|
||||
console.error('imageView is null');
|
||||
}
|
||||
|
||||
const imagesContainer = document.querySelector('.images');
|
||||
if (!imagesContainer) {
|
||||
console.error('imagesContainer is null');
|
||||
}
|
||||
|
||||
function openImageViewer(element) {
|
||||
initializeImageList();
|
||||
initializeImageList(); // Update the image list
|
||||
|
||||
const parentImageDiv = element.closest('.image');
|
||||
if (!parentImageDiv) return;
|
||||
|
||||
|
@ -76,62 +61,75 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
document.body.classList.add('viewer-open');
|
||||
viewerOverlay.style.display = 'block';
|
||||
|
||||
imageView.classList.replace('image_hide', 'image_show');
|
||||
imageView.classList.remove('image_hide');
|
||||
imageView.classList.add('image_show');
|
||||
}
|
||||
|
||||
let fullImageUrl, sourceUrl, proxyFullUrl;
|
||||
|
||||
function displayImage(index) {
|
||||
if (index < 0 || index >= imageList.length) return;
|
||||
|
||||
// Remove the `.image_selected` class from all images
|
||||
imageList.forEach(img => {
|
||||
const parentImageDiv = img.closest('.image');
|
||||
parentImageDiv?.classList.remove('image_selected');
|
||||
if (parentImageDiv) {
|
||||
parentImageDiv.classList.remove('image_selected');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const imgElement = imageList[index];
|
||||
const parentImageDiv = imgElement.closest('.image');
|
||||
parentImageDiv?.classList.add('image_selected');
|
||||
|
||||
fullImageUrl = imgElement.getAttribute('data-full') || imgElement.src;
|
||||
sourceUrl = imgElement.getAttribute('data-source');
|
||||
proxyFullUrl = imgElement.getAttribute('data-proxy-full') || fullImageUrl;
|
||||
if (!parentImageDiv) {
|
||||
console.warn('Parent image div not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the `.image_selected` class to the currently displayed image
|
||||
parentImageDiv.classList.add('image_selected');
|
||||
|
||||
// Use the `data-full` attribute for the full image URL
|
||||
let fullImageUrl = imgElement.getAttribute('data-full') || imgElement.src;
|
||||
const title = imgElement.alt || 'Untitled';
|
||||
|
||||
// Get the source URL from the data-source attribute
|
||||
const sourceUrl = imgElement.getAttribute('data-source');
|
||||
|
||||
// Fallback logic: if sourceUrl is null, use `data-proxy-full` or a meaningful default
|
||||
const proxyFullUrl = imgElement.getAttribute('data-proxy-full') || fullImageUrl;
|
||||
|
||||
// Elements in the viewer
|
||||
const viewerImage = document.getElementById('viewer-image');
|
||||
const viewerTitle = document.getElementById('viewer-title');
|
||||
|
||||
viewerTitle.textContent = imgElement.alt || 'Untitled';
|
||||
|
||||
viewerImage.onerror = () => viewerImage.src = proxyFullUrl;
|
||||
viewerImage.onload = () => {};
|
||||
|
||||
const fullSizeLink = document.getElementById('viewer-full-size-link');
|
||||
const proxySizeLink = document.getElementById('viewer-proxy-size-link');
|
||||
|
||||
viewerTitle.textContent = title;
|
||||
fullSizeLink.href = sourceUrl || proxyFullUrl;
|
||||
|
||||
// Remove previous event listeners to avoid stacking
|
||||
viewerImage.onload = null;
|
||||
viewerImage.onerror = null;
|
||||
|
||||
// Set up the error handler to switch to the proxy image if the full image fails to load
|
||||
viewerImage.onerror = function() {
|
||||
// Use the proxy image as a fallback
|
||||
viewerImage.src = proxyFullUrl;
|
||||
proxySizeLink.href = proxyFullUrl;
|
||||
};
|
||||
|
||||
// Set up the load handler to ensure the proxySizeLink is set correctly if the image loads
|
||||
viewerImage.onload = function() {
|
||||
proxySizeLink.href = fullImageUrl;
|
||||
};
|
||||
|
||||
// Start loading the image
|
||||
viewerImage.src = fullImageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('viewer-copy-link').onclick = () => {
|
||||
navigator.clipboard.writeText(window.location.origin + fullImageUrl).catch(console.error);
|
||||
};
|
||||
document.body.addEventListener('click', function(e) {
|
||||
let target = e.target;
|
||||
let clickableElement = target.closest('img.clickable, .img_title.clickable');
|
||||
|
||||
document.getElementById('viewer-open-image').onclick = () => {
|
||||
window.open(fullImageUrl, '_blank');
|
||||
};
|
||||
|
||||
document.getElementById('viewer-open-source').onclick = () => {
|
||||
window.open(sourceUrl || proxyFullUrl, '_blank');
|
||||
};
|
||||
|
||||
document.getElementById('viewer-download-image').onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
const a = document.createElement('a');
|
||||
a.href = fullImageUrl;
|
||||
a.download = fullImageUrl.split('/').pop();
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
};
|
||||
|
||||
document.body.addEventListener('click', e => {
|
||||
const clickableElement = e.target.closest('img.clickable, .img_title.clickable');
|
||||
if (clickableElement) {
|
||||
e.preventDefault();
|
||||
openImageViewer(clickableElement);
|
||||
|
@ -139,31 +137,65 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
|
||||
function closeImageViewer() {
|
||||
imageView.classList.replace('image_show', 'image_hide');
|
||||
imageView.classList.remove('image_show');
|
||||
imageView.classList.add('image_hide');
|
||||
viewerOpen = false;
|
||||
currentIndex = -1;
|
||||
|
||||
imagesContainer.classList.add('images_viewer_hidden');
|
||||
document.body.classList.remove('viewer-open');
|
||||
viewerOverlay.style.display = 'none';
|
||||
|
||||
imageList.forEach(img => img.closest('.image')?.classList.remove('image_selected'));
|
||||
|
||||
// Remove `.image_selected` from all images
|
||||
imageList.forEach(img => {
|
||||
const parentImageDiv = img.closest('.image');
|
||||
if (parentImageDiv) {
|
||||
parentImageDiv.classList.remove('image_selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('viewer-close-button').onclick = closeImageViewer;
|
||||
document.getElementById('viewer-prev-button').onclick = () => currentIndex > 0 && displayImage(--currentIndex);
|
||||
document.getElementById('viewer-next-button').onclick = () => currentIndex < imageList.length - 1 && displayImage(++currentIndex);
|
||||
// Navigation functions
|
||||
function showPreviousImage() {
|
||||
if (currentIndex > 0) {
|
||||
currentIndex--;
|
||||
displayImage(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
if (viewerOpen && !viewerOverlay.contains(e.target) && !e.target.closest('.image')) {
|
||||
closeImageViewer();
|
||||
function showNextImage() {
|
||||
if (currentIndex < imageList.length - 1) {
|
||||
currentIndex++;
|
||||
displayImage(currentIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners for navigation and closing
|
||||
document.getElementById('viewer-close-button').addEventListener('click', closeImageViewer);
|
||||
document.getElementById('viewer-prev-button').addEventListener('click', showPreviousImage);
|
||||
document.getElementById('viewer-next-button').addEventListener('click', showNextImage);
|
||||
|
||||
// Close viewer when clicking outside the image
|
||||
document.addEventListener('click', function(e) {
|
||||
if (viewerOpen) {
|
||||
const target = e.target;
|
||||
const clickedInsideViewer = viewerOverlay.contains(target) || target.closest('.image');
|
||||
if (!clickedInsideViewer) {
|
||||
closeImageViewer();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
if (!viewerOpen) return;
|
||||
if (e.key === 'Escape') closeImageViewer();
|
||||
if (e.key === 'ArrowLeft' && currentIndex > 0) displayImage(--currentIndex);
|
||||
if (e.key === 'ArrowRight' && currentIndex < imageList.length - 1) displayImage(++currentIndex);
|
||||
});
|
||||
});
|
||||
// Handle keyboard events for closing and navigation
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (viewerOpen) {
|
||||
if (e.key === 'Escape') {
|
||||
closeImageViewer();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
showPreviousImage();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
showNextImage();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,9 +24,9 @@
|
|||
<button class="material-icons-round clickable settings-icon-link settings-icon-link-search"></button>
|
||||
</div>
|
||||
<div class="search-menu settings-menu-hidden">
|
||||
<h2>{{ translate "settings" }}</h2>
|
||||
<h2>Settings</h2>
|
||||
<div class="settings-content">
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">{{ translate "all_settings" }}</button>
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">All settings</button>
|
||||
<div class="theme-settings">
|
||||
<p><span class="highlight">Current theme: </span> <span id="theme_name">{{.Theme}}</span></p>
|
||||
<div class="themes-settings-menu">
|
||||
|
@ -65,8 +65,8 @@
|
|||
src="/static/images/icon.svg"
|
||||
alt="QGato"
|
||||
>
|
||||
<h2>{{ translate "site_name" }}</h2>
|
||||
<p>{{ translate "site_description" }}</p>
|
||||
<h2>QGato</h2>
|
||||
<p>A open-source private search engine.</p>
|
||||
<div class="button-container">
|
||||
<button onclick="window.location.href='https://weforge.xyz/Spitfire/Search'">Source Code</button>
|
||||
<button onclick="window.location.href='/privacy'">Privacy policy</button>
|
||||
|
@ -103,10 +103,6 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button> <!-- Video icon -->
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "category_music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button> <!-- Forum icon -->
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
|
@ -124,6 +120,8 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<p class="fetched fetched_dif fetched_tor">{{ translate "fetched_in" .Fetched }}</p>
|
||||
|
||||
{{ if .Results }}
|
||||
<form action="/search" class="torrent-sort" method="GET">
|
||||
<input type="hidden" name="q" value="{{ .Query }}">
|
||||
|
@ -149,8 +147,6 @@
|
|||
</select>
|
||||
<button type="submit" class="torrent-sort-save">{{ translate "apply_settings" }}</button>
|
||||
</form>
|
||||
<p class="fetched fetched_dif_files fetched_tor">{{ translate "fetched_in" .Fetched }}</p>
|
||||
|
||||
<div class="clean">
|
||||
{{ range .Results }}
|
||||
<div class="results" id="results">
|
||||
|
@ -158,7 +154,7 @@
|
|||
<div class="error">{{ translate "error" }}: {{ .Error }}</div>
|
||||
{{ else }}
|
||||
<a id="link" href="{{ .URL }}">{{ .URL }}</a>
|
||||
<a class="torrent" href="magnet:{{ .Magnet }}"><h3 class="single-line-ellipsis">{{ .Title }}</h3></a>
|
||||
<a class="torrent" href="magnet:{{ .Magnet }}"><h3>{{ .Title }}</h3></a>
|
||||
<p class="stats">{{ if .Views }}{{ .Views }} {{ translate "views" }} • {{ end }}{{ .Size }}</p>
|
||||
<p class="publish__info">{{ translate "seeders" }}: <span class="seeders">{{ .Seeders }}</span> | {{ translate "leechers" }}: <span class="leechers">{{ .Leechers }}</span></p>
|
||||
{{ end }}
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
<link rel="stylesheet" href="/static/css/style-fonts.css">
|
||||
<link rel="stylesheet" href="/static/css/style-loadingindicator.css">
|
||||
<link rel="stylesheet" href="/static/css/style-menu.css">
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="{{ translate "site_name" }}" href="/opensearch.xml">
|
||||
<!-- Icons -->
|
||||
|
@ -25,9 +24,9 @@
|
|||
<button class="material-icons-round clickable settings-icon-link settings-icon-link-search"></button>
|
||||
</div>
|
||||
<div class="search-menu settings-menu-hidden">
|
||||
<h2>{{ translate "settings" }}</h2>
|
||||
<h2>Settings</h2>
|
||||
<div class="settings-content">
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">{{ translate "all_settings" }}</button>
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">All settings</button>
|
||||
<div class="theme-settings">
|
||||
<p><span class="highlight">Current theme: </span> <span id="theme_name">{{.Theme}}</span></p>
|
||||
<div class="themes-settings-menu">
|
||||
|
@ -66,8 +65,8 @@
|
|||
src="/static/images/icon.svg"
|
||||
alt="QGato"
|
||||
>
|
||||
<h2>{{ translate "site_name" }}</h2>
|
||||
<p>{{ translate "site_description" }}</p>
|
||||
<h2>QGato</h2>
|
||||
<p>A open-source private search engine.</p>
|
||||
<div class="button-container">
|
||||
<button onclick="window.location.href='https://weforge.xyz/Spitfire/Search'">Source Code</button>
|
||||
<button onclick="window.location.href='/privacy'">Privacy policy</button>
|
||||
|
@ -104,10 +103,6 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button> <!-- Video icon -->
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "category_music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable search-active" name="t" value="forum"></button> <!-- Forum icon -->
|
||||
<button name="t" value="forum" class="clickable search-active">{{ translate "forums" }}</button>
|
||||
|
@ -137,15 +132,13 @@
|
|||
</select>
|
||||
<button class="results-save" name="t" value="forum">{{ translate "save_settings" }}</button>
|
||||
</form>
|
||||
<p class="fetched fetched_dif_files fetched_tor">{{ translate "fetched_in" .Fetched }}</p>
|
||||
|
||||
<div class="results" id="results">
|
||||
{{if .Results}}
|
||||
{{range .Results}}
|
||||
<div class="result_item">
|
||||
<a id="link" class="single-line-ellipsis" href="{{.URL}}">{{.URL}}</a>
|
||||
<a href="{{.URL}}"><h3 class="single-line-ellipsis">{{.Header}}</h3></a>
|
||||
<p class="clamp-3-lines">{{.Description}}</p>
|
||||
<a id="link" href="{{.URL}}">{{.URL}}</a>
|
||||
<a href="{{.URL}}"><h3>{{.Header}}</h3></a>
|
||||
<p>{{.Description}}</p>
|
||||
</div>
|
||||
<br>
|
||||
{{end}}
|
||||
|
@ -158,8 +151,8 @@
|
|||
<div class="no-results-found">{{ translate "no_more_results" }}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="message-bottom-right" class="message-bottom-right">
|
||||
<span id="loading-text">{{ translate "searching_for_new_results" }}</span><span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>
|
||||
<div class="message-bottom-left" id="message-bottom-left">
|
||||
<span>{{ translate "searching_for_new_results" }}</span>
|
||||
</div>
|
||||
<div class="prev-next prev-img" id="prev-next">
|
||||
<form action="/search" method="get">
|
||||
|
|
|
@ -15,10 +15,8 @@
|
|||
}
|
||||
</style>
|
||||
</noscript>
|
||||
<link rel="stylesheet" href="/static/css/style-imageloading.css">
|
||||
<link rel="stylesheet" href="/static/css/style-imageviewer.css">
|
||||
<link rel="stylesheet" href="/static/css/style-fixedwidth.css">
|
||||
<link rel="stylesheet" href="/static/css/style-loadingindicator.css">
|
||||
<link rel="stylesheet" href="/static/css/style-menu.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
<link rel="stylesheet" href="/static/css/style-fonts.css">
|
||||
|
@ -35,9 +33,9 @@
|
|||
<button class="material-icons-round clickable settings-icon-link settings-icon-link-search"></button>
|
||||
</div>
|
||||
<div class="search-menu settings-menu-hidden">
|
||||
<h2>{{ translate "settings" }}</h2>
|
||||
<h2>Settings</h2>
|
||||
<div class="settings-content">
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">{{ translate "all_settings" }}</button>
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">All settings</button>
|
||||
<div class="theme-settings">
|
||||
<p><span class="highlight">Current theme: </span> <span id="theme_name">{{.Theme}}</span></p>
|
||||
<div class="themes-settings-menu">
|
||||
|
@ -76,8 +74,8 @@
|
|||
src="/static/images/icon.svg"
|
||||
alt="QGato"
|
||||
>
|
||||
<h2>{{ translate "site_name" }}</h2>
|
||||
<p>{{ translate "site_description" }}</p>
|
||||
<h2>QGato</h2>
|
||||
<p>A open-source private search engine.</p>
|
||||
<div class="button-container">
|
||||
<button onclick="window.location.href='https://weforge.xyz/Spitfire/Search'">Source Code</button>
|
||||
<button onclick="window.location.href='/privacy'">Privacy policy</button>
|
||||
|
@ -115,10 +113,6 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button>
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "category_music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button>
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
|
@ -152,8 +146,7 @@
|
|||
<button class="results-save" name="t" value="image">{{ translate "save_settings" }}</button>
|
||||
</form>
|
||||
<div class="search-results" id="results">
|
||||
|
||||
<p class="fetched fetched_dif_images fetched_vid">{{ translate "fetched_in" .Fetched }}</p>
|
||||
|
||||
<!-- Results go here -->
|
||||
{{ if .Results }}
|
||||
<div class="images images_viewer_hidden">
|
||||
|
@ -211,7 +204,7 @@
|
|||
{{ end }}
|
||||
<div class="resolution">{{ $result.Width }} × {{ $result.Height }}</div>
|
||||
<div class="details">
|
||||
<span class="img_title clickable single-line-ellipsis">{{ $result.Title }}</span>
|
||||
<span class="img_title clickable">{{ $result.Title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
@ -241,8 +234,8 @@
|
|||
<div class="no-results-found no-results-found-offset">{{ translate "no_more_results" }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div id="message-bottom-right" class="message-bottom-right">
|
||||
<span id="loading-text">{{ translate "searching_for_new_results" }}</span><span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>
|
||||
<div class="message-bottom-left" id="message-bottom-left">
|
||||
<span>{{ translate "searching_for_new_results" }}</span>
|
||||
</div>
|
||||
|
||||
<div id="image-viewer-overlay" style="display: none;"></div>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
{{ end }}
|
||||
<div class="resolution">{{ $result.Width }} × {{ $result.Height }}</div>
|
||||
<div class="details">
|
||||
<span class="img_title clickable single-line-ellipsis">{{ $result.Title }}</span>
|
||||
<span class="img_title clickable">{{ $result.Title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
|
@ -39,9 +39,9 @@
|
|||
<button class="material-icons-round clickable settings-icon-link settings-icon-link-search"></button>
|
||||
</div>
|
||||
<div class="search-menu settings-menu-hidden">
|
||||
<h2>{{ translate "settings" }}</h2>
|
||||
<h2>Settings</h2>
|
||||
<div class="settings-content">
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">{{ translate "all_settings" }}</button>
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">All settings</button>
|
||||
<div class="theme-settings">
|
||||
<p><span class="highlight">Current theme: </span> <span id="theme_name">{{.Theme}}</span></p>
|
||||
<div class="themes-settings-menu">
|
||||
|
@ -80,8 +80,8 @@
|
|||
src="/static/images/icon.svg"
|
||||
alt="QGato"
|
||||
>
|
||||
<h2>{{ translate "site_name" }}</h2>
|
||||
<p>{{ translate "site_description" }}</p>
|
||||
<h2>QGato</h2>
|
||||
<p>A open-source private search engine.</p>
|
||||
<div class="button-container">
|
||||
<button onclick="window.location.href='https://weforge.xyz/Spitfire/Search'">Source Code</button>
|
||||
<button onclick="window.location.href='/privacy'">Privacy policy</button>
|
||||
|
@ -118,10 +118,6 @@
|
|||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button> <!-- Movie icon -->
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable">{{ translate "category_music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button> <!-- Forum icon -->
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
|
|
|
@ -1,192 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{{ if .IsThemeDark }}
|
||||
<meta name="darkreader-lock">
|
||||
{{ end }}
|
||||
<title>{{ .Query }} - Music Search - {{ translate "site_name" }}</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<link rel="stylesheet" href="/static/css/{{.Theme}}.css">
|
||||
<link rel="stylesheet" href="/static/css/style-fonts.css">
|
||||
<link rel="stylesheet" href="/static/css/style-music.css">
|
||||
<link rel="stylesheet" href="/static/css/style-loadingindicator.css">
|
||||
<link rel="stylesheet" href="/static/css/style-menu.css">
|
||||
<link rel="search" type="application/opensearchdescription+xml" title="{{ translate "site_name" }}" href="/opensearch.xml">
|
||||
<!-- Icons -->
|
||||
<link rel="icon" href="{{ .IconPathSVG }}" type="image/svg+xml">
|
||||
<link rel="icon" href="{{ .IconPathPNG }}" type="image/png">
|
||||
<link rel="apple-touch-icon" href="{{ .IconPathPNG }}">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Menu Button -->
|
||||
<div id="content" class="js-enabled">
|
||||
<div class="settings-search-div settings-search-div-search">
|
||||
<button class="material-icons-round clickable settings-icon-link settings-icon-link-search"></button>
|
||||
</div>
|
||||
<div class="search-menu settings-menu-hidden">
|
||||
<h2>{{ translate "settings" }}</h2>
|
||||
<div class="settings-content">
|
||||
<button id="settingsButton" onclick="window.location.href='/settings'">{{ translate "all_settings" }}</button>
|
||||
<div class="theme-settings">
|
||||
<p><span class="highlight">Current theme: </span> <span id="theme_name">{{.Theme}}</span></p>
|
||||
<div class="themes-settings-menu">
|
||||
<div><img class="view-image-search clickable" id="dark_theme" alt="Dark Theme" src="/static/images/dark.webp"></div>
|
||||
<div><img class="view-image-search clickable" id="light_theme" alt="Light Theme" src="/static/images/light.webp"></div>
|
||||
</div>
|
||||
</div>
|
||||
<select class="lang" name="safe" id="safeSearchSelect">
|
||||
<option value="disabled" {{if eq .Safe "disabled"}}selected{{end}}>Safe Search Off</option>
|
||||
<option value="active" {{if eq .Safe "active"}}selected{{end}}>Safe Search On</option>
|
||||
</select>
|
||||
<select class="lang" name="lang" id="languageSelect">
|
||||
{{range .LanguageOptions}}
|
||||
<option value="{{.Code}}" {{if eq .Code $.CurrentLang}}selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
<button id="aboutQGatoBtn">About QGato</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<noscript>
|
||||
<div class="settings-search-div settings-search-div-search">
|
||||
<a href="/settings" class="material-icons-round clickable settings-icon-link settings-icon-link-search"></a>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<!-- Popup Modal for QGato -->
|
||||
<div id="aboutQGatoModal">
|
||||
<!-- Close Button -->
|
||||
<button class="btn-nostyle" id="close-button">
|
||||
<div class="material-icons-round icon_visibility clickable cloase-btn"></div>
|
||||
</button>
|
||||
|
||||
<div class="modal-content">
|
||||
<img
|
||||
src="/static/images/icon.svg"
|
||||
alt="QGato"
|
||||
>
|
||||
<h2>{{ translate "site_name" }}</h2>
|
||||
<p>{{ translate "site_description" }}</p>
|
||||
<div class="button-container">
|
||||
<button onclick="window.location.href='https://weforge.xyz/Spitfire/Search'">Source Code</button>
|
||||
<button onclick="window.location.href='/privacy'">Privacy policy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="/music" id="prev-next-form" class="results-search-container" method="GET" autocomplete="off">
|
||||
<h1 class="logomobile">
|
||||
<div class="logo-container" href="/">
|
||||
<a href="/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="29" height="86" viewBox="0 0 29 86"><path fill-rule="evenodd" d="M-44.35.78C-70.8 6.76-74.8 43.17-50.67 55.73c1.7.88 4.42 1.7 7.83 2.22 4.48.68 9.6.86 9.58.15-.04-1.43-7.3-8.28-8.67-8.28-3.15 0-9.94-5.66-11.97-10C-61.95 22.66-48.1 4.54-31.12 10c13.5 4.34 18.1 22.7 8.66 34.44-1.85 2.3-1.75 2.3-4.4-.22-4.8-4.59-8.57-5.25-11.98-2.1-2.18 2-2.15 2.66.15 4.14 1.9 1.22 13.4 12.95 17.49 17.83 4.3 5.13 5.24 6.14 7.52 7.97C-9.25 75.6-1.23 77.91 1 76.28c.67-.5 1.86-7.8 1.35-8.3-.12-.12-1.34-.4-2.7-.61-5.36-.86-9.23-3.46-14.2-9.55-3.49-4.27-4.12-5.26-3.38-5.26 2.54 0 8.05-8.62 9.86-15.43C-2.36 15.63-22.18-4.23-44.35.78m65.13 1.53C4.92 6.02-4.86 22.36-.72 38.24 3 52.62 18.43 59.63 33.67 57.64c4.7-.62 2.43-.66 4.45-.8s6.45-.01 6.93-.2c.4.03.72-.45.72-.94V42.31c0-7.36-.16-13.62-.33-13.9-.26-.4-2.36-.49-10.19-.4-11.44.15-10.96-.03-10.96 4.09 0 2.44-.04 3.99 1.17 4.7 1.13.68 3.43.59 6.68.41l3.76-.2.27 5.68c.33 6.59.57 6.15-3.64 6.7-15.53 2.04-24-5.02-23.37-19.43.66-15.1 12.2-22.78 26.96-17.94 4.5 1.47 4.4 1.52 6.16-2.8 1.5-3.68 1.5-3.69-.82-4.69C36.03 2.2 25.9 1.11 20.78 2.31m78.83.8c-2.87.76-2.9.84-3.15 6.12-.25 5.56.12 4.96-3.35 5.29-3.43.32-3.32.15-2.76 4.2.61 4.37.6 4.34 3.76 4.34h2.65v12.7c0 14.5 1.55 16.33 3.5 18.3 3.6 3.48 9.59 4.92 14.93 3.06 2.45-.85 2.43-.8 2.18-4.95-.25-4.1-.43-3.5-3.16-2.91-7.73 1.64-8.27.6-8.27-15.05V22.87h5.66l5.34-.1c.67-.01.97.4 1.28-3.9.35-4.8-.2-4.01-.8-4.14l-5.82.18-5.66.26v-5.16c0-5.84-.2-6.48-2.25-7.04-1.75-.49-1.76-.49-4.08.13m-34.5 11.02c-2.64.38-4.71 1.04-8.54 2.72l-4.03 1.76c-1.09.39-.28 1.29.69 3.89 1.06 2.75 1.35 3.35 2.11 3.03.76-.32.7-.23 1.43-.65 9.08-5.25 20.26-2.63 20.26 4.74v2.14l-5.95.2c-13.84.48-20.29 4.75-20.38 13.51-.13 12.4 14.18 17.22 24.62 8.3l2.3-1.97.23 1.85c.32 2.53.6 3.06 2.04 3.67 1.42.6 7.16.62 7.75.03.77-.77.37-6-.25-6.34-.94-.5-.77-1.57-.88-12.63-.15-14.87-.5-16.5-4.4-20.13-3.03-2.84-11.55-4.9-17-4.12m72.86 0c-27.2 5.27-24.13 43.96 3.47 43.9 14.67-.04 24.4-12.77 21.53-28.16-1.86-9.95-14.33-17.8-25-15.73m8.29 8.96c6.88 2.34 9.61 11.51 5.9 19.79-4.13 9.19-17.89 9.17-22.14-.03-1.32-2.85-1.24-10.79.14-13.54 3-6 9.45-8.49 16.1-6.22m-68.84 18.5v3.09l-1.85 1.63c-7.46 6.58-16.36 5.49-15.6-1.9.45-4.35 3.62-5.77 13.06-5.87l4.4-.05v3.1" style="fill:currentColor" transform="translate(-31.68 4.9)"/><path d="M-13.47 73.3v1.11q-.65-.3-1.23-.46-.57-.15-1.11-.15-.93 0-1.44.36-.5.36-.5 1.03 0 .56.33.85.34.28 1.28.46l.69.14q1.27.24 1.88.86.6.6.6 1.64 0 1.22-.82 1.86-.82.63-2.4.63-.6 0-1.28-.14-.68-.13-1.4-.4v-1.17q.7.39 1.36.58.67.2 1.31.2.98 0 1.51-.38.54-.39.54-1.1 0-.62-.39-.97-.38-.35-1.25-.53l-.7-.13q-1.27-.26-1.84-.8-.57-.54-.57-1.51 0-1.12.78-1.76.8-.65 2.18-.65.6 0 1.2.1.63.12 1.27.33zm2.29-.28h5.34V74h-4.2v2.5h4.02v.96h-4.02v3.05h4.3v.97h-5.44zm10.14 1.13-1.55 4.2H.5zm-.65-1.13h1.3l3.21 8.45H1.64L.87 79.3h-3.8l-.78 2.17h-1.2zm9.75 4.48q.37.13.71.54.35.4.7 1.12l1.16 2.3H9.41L8.33 79.3q-.42-.85-.82-1.13-.39-.27-1.07-.27H5.2v3.57H4.06v-8.45h2.58q1.44 0 2.16.6.7.61.7 1.84 0 .8-.36 1.32-.37.52-1.08.73zM5.2 73.97v3h1.44q.82 0 1.24-.38.42-.38.42-1.12 0-.75-.42-1.12-.42-.38-1.24-.38zm12.65-.3v1.2q-.58-.53-1.23-.8-.65-.26-1.39-.26-1.45 0-2.22.89-.77.88-.77 2.55t.77 2.56q.77.88 2.22.88.74 0 1.39-.26.65-.27 1.23-.8v1.19q-.6.4-1.27.6-.67.21-1.42.21-1.91 0-3.02-1.17-1.1-1.18-1.1-3.2 0-2.04 1.1-3.21 1.1-1.18 3.02-1.18.76 0 1.43.2.67.2 1.26.6zm1.76-.65h1.14v3.46h4.15v-3.46h1.15v8.45H24.9v-4.02h-4.15v4.02h-1.14zm12.39 0h5.34V74h-4.2v2.5h4.02v.96h-4.02v3.05h4.3v.97H32zm7.32 0h1.53l3.75 7.07v-7.07h1.1v8.45h-1.53l-3.74-7.07v7.07h-1.11zm14.42 7.24V78h-1.87v-.93h3v3.62q-.67.47-1.46.71-.8.24-1.7.24-1.98 0-3.1-1.15-1.12-1.16-1.12-3.23 0-2.07 1.12-3.22 1.12-1.16 3.1-1.16.82 0 1.56.2.75.2 1.38.6v1.22q-.64-.54-1.35-.8-.71-.28-1.5-.28-1.55 0-2.33.86-.77.87-.77 2.58t.77 2.58q.78.86 2.33.86.6 0 1.08-.1.48-.1.86-.33zm3.21-7.24h1.14v8.45h-1.14zm3.42 0h1.54l3.74 7.07v-7.07h1.1v8.45h-1.53l-3.74-7.07v7.07h-1.11zm8.66 0h5.34V74h-4.2v2.5h4.02v.96h-4.02v3.05h4.3v.97h-5.44z" aria-label="SEARCH ENGINE" style="font-family:'ADLaM Display';white-space:pre;fill:currentColor"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</h1>
|
||||
<div class="wrapper-results">
|
||||
<input type="text" name="q" value="{{ .Query }}" id="search-input" />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="music"></button>
|
||||
<div class="autocomplete">
|
||||
<ul></ul>
|
||||
</div>
|
||||
<input type="submit" class="hide" name="t" value="music" />
|
||||
</div>
|
||||
<div class="sub-search-button-wrapper">
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="text"></button>
|
||||
<button name="t" value="text" class="clickable">{{ translate "web" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="image"></button>
|
||||
<button name="t" value="image" class="clickable">{{ translate "images" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="video"></button>
|
||||
<button name="t" value="video" class="clickable">{{ translate "videos" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable search-active" name="t" value="music"></button>
|
||||
<button name="t" value="music" class="clickable search-active">{{ translate "category_music" }}</button>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="forum"></button>
|
||||
<button name="t" value="forum" class="clickable">{{ translate "forums" }}</button>
|
||||
</div>
|
||||
<div id="content2" class="js-enabled">
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="map"></button>
|
||||
<button name="t" value="map" class="clickable">{{ translate "maps" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-container-results-btn">
|
||||
<button id="sub-search-wrapper-ico" class="material-icons-round clickable" name="t" value="file"></button>
|
||||
<button name="t" value="file" class="clickable">{{ translate "torrents" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<form class="results_settings" action="/music" method="get">
|
||||
</form>
|
||||
<p class="fetched fetched_dif_files">{{ translate "fetched_in" .Fetched }}</p>
|
||||
|
||||
<div class="results" id="results">
|
||||
{{if .Results}}
|
||||
{{range .Results}}
|
||||
<div class="result-item music-item">
|
||||
<div class="music-thumbnail">
|
||||
<a href="{{.URL}}">
|
||||
<img src="{{.Thumbnail}}" alt="{{.Title}} thumbnail" loading="lazy">
|
||||
{{if .Duration}}<div class="duration-overlay">{{.Duration}}</div>{{end}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="music-info">
|
||||
<a href="{{.URL}}"><h3 class="video_title single-line-ellipsis">{{.Title}}</h3></a>
|
||||
<div class="stats">
|
||||
<span class="artist">{{.Artist}}</span>
|
||||
<span class="pipe">|</span>
|
||||
<span class="source">{{.Source}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{else if .NoResults}}
|
||||
<div class="no-results-found">
|
||||
{{ translate "no_results_found" .Query }}<br>
|
||||
{{ translate "suggest_rephrase" }}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="no-results-found">{{ translate "no_more_results" }}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div id="message-bottom-right" class="message-bottom-right">
|
||||
<span id="loading-text">{{ translate "searching_for_new_results" }}</span><span class="dot">.</span><span class="dot">.</span><span class="dot">.</span>
|
||||
</div>
|
||||
<div class="prev-next prev-img" id="prev-next">
|
||||
<form action="/music" method="get">
|
||||
<input type="hidden" name="q" value="{{ .Query }}">
|
||||
<input type="hidden" name="t" value="music">
|
||||
<noscript>
|
||||
{{ if .HasPrevPage }}
|
||||
<button type="submit" name="p" value="{{ sub .Page 1 }}">{{ translate "previous" }}</button>
|
||||
{{ end }}
|
||||
{{ if .HasNextPage }}
|
||||
<button type="submit" name="p" value="{{ add .Page 1 }}">{{ translate "next" }}</button>
|
||||
{{ end }}
|
||||
</noscript>
|
||||
</form>
|
||||
</div>
|
||||
<div id="template-data" data-page="{{ .Page }}" data-query="{{ .Query }}" data-type="music"></div>
|
||||
<script defer src="/static/js/dynamicscrolling.js"></script>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
<script defer src="/static/js/minimenu.js"></script>
|
||||
<script>
|
||||
document.querySelectorAll('.js-enabled').forEach(el => el.classList.remove('js-enabled'));
|
||||
|
||||
// Handle music service selection
|
||||
document.getElementById('musicServiceSelect').addEventListener('change', function() {
|
||||
const form = this.closest('form');
|
||||
form.submit();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue