diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml deleted file mode 100644 index ae2a269..0000000 --- a/.forgejo/workflows/release.yaml +++ /dev/null @@ -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 }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c731c6b..5f5aeab 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,4 @@ cache/ *.min.js *.min.css qgato -qgato.exe test.py \ No newline at end of file diff --git a/README.md b/README.md index af48ace..5ad3337 100644 --- a/README.md +++ b/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 diff --git a/agent.go b/agent.go index 90fb669..6333102 100755 --- a/agent.go +++ b/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 } diff --git a/cache-images.go b/cache-images.go index 84a4256..4e551cd 100644 --- a/cache-images.go +++ b/cache-images.go @@ -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) -} diff --git a/cache.go b/cache.go index f769066..ac2902d 100644 --- a/cache.go +++ b/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 } diff --git a/common.go b/common.go index 75baf40..75f6b91 100755 --- a/common.go +++ b/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(`%s`, template.HTMLEscapeString(domain))) - - if path != "" { - pathDisplay := strings.ReplaceAll(path, "/", " › ") - lp.Path = template.HTML(fmt.Sprintf(` › %s`, 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 -} diff --git a/config.go b/config.go index c9f40b6..18d83cf 100644 --- a/config.go +++ b/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 diff --git a/crawler-extraction.go b/crawler-extraction.go index 7fe2591..4ce8b9d 100644 --- a/crawler-extraction.go +++ b/crawler-extraction.go @@ -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() diff --git a/disk.go b/disk.go deleted file mode 100644 index 0eafde9..0000000 --- a/disk.go +++ /dev/null @@ -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) -} diff --git a/disk_win.go b/disk_win.go deleted file mode 100644 index 394549d..0000000 --- a/disk_win.go +++ /dev/null @@ -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 -} diff --git a/favicon.go b/favicon.go deleted file mode 100644 index 37c7d94..0000000 --- a/favicon.go +++ /dev/null @@ -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(`]+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 -} diff --git a/files-thepiratebay.go b/files-thepiratebay.go index 3045bf6..b98ee27 100644 --- a/files-thepiratebay.go +++ b/files-thepiratebay.go @@ -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 diff --git a/files-torrentgalaxy.go b/files-torrentgalaxy.go index 5bcd05e..51f51ca 100644 --- a/files-torrentgalaxy.go +++ b/files-torrentgalaxy.go @@ -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) } diff --git a/files.go b/files.go index 2eea7bb..d0c1ff1 100755 --- a/files.go +++ b/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 diff --git a/forums.go b/forums.go index 660b3b8..bd57e55 100755 --- a/forums.go +++ b/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)) diff --git a/go.mod b/go.mod index b088e24..f7d89ad 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 752f0ed..66cede6 100644 --- a/go.sum +++ b/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= diff --git a/images-bing.go b/images-bing.go index f057ac5..b6a6aa6 100644 --- a/images-bing.go +++ b/images-bing.go @@ -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) } diff --git a/images-deviantart.go b/images-deviantart.go index 171ac1a..3077640 100644 --- a/images-deviantart.go +++ b/images-deviantart.go @@ -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") } diff --git a/images-imgur.go b/images-imgur.go index e085371..641f645 100644 --- a/images-imgur.go +++ b/images-imgur.go @@ -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 "" diff --git a/images-quant.go b/images-quant.go index ab5d677..d85d0f9 100644 --- a/images-quant.go +++ b/images-quant.go @@ -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 } diff --git a/images.go b/images.go index ef03f8b..a044013 100755 --- a/images.go +++ b/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) diff --git a/init.go b/init.go index 87dc0ce..bf0d220 100644 --- a/init.go +++ b/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.") diff --git a/lang/af/LC_MESSAGES/default.po b/lang/af/LC_MESSAGES/default.po index 928260a..57fdf5d 100644 --- a/lang/af/LC_MESSAGES/default.po +++ b/lang/af/LC_MESSAGES/default.po @@ -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" diff --git a/lang/ar/LC_MESSAGES/default.po b/lang/ar/LC_MESSAGES/default.po index 78c7fcd..65ce544 100644 --- a/lang/ar/LC_MESSAGES/default.po +++ b/lang/ar/LC_MESSAGES/default.po @@ -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 "ميلي ثانية" diff --git a/lang/be/LC_MESSAGES/default.po b/lang/be/LC_MESSAGES/default.po index 2ec3349..181b9a6 100644 --- a/lang/be/LC_MESSAGES/default.po +++ b/lang/be/LC_MESSAGES/default.po @@ -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 "Мілісекунды" + \ No newline at end of file diff --git a/lang/bg/LC_MESSAGES/default.po b/lang/bg/LC_MESSAGES/default.po index f54b414..d2d7806 100644 --- a/lang/bg/LC_MESSAGES/default.po +++ b/lang/bg/LC_MESSAGES/default.po @@ -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 "Милисекунди" diff --git a/lang/ca/LC_MESSAGES/default.po b/lang/ca/LC_MESSAGES/default.po index e0480c4..c71e54e 100644 --- a/lang/ca/LC_MESSAGES/default.po +++ b/lang/ca/LC_MESSAGES/default.po @@ -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" diff --git a/lang/cs/LC_MESSAGES/default.po b/lang/cs/LC_MESSAGES/default.po index fbd69d6..52569b5 100644 --- a/lang/cs/LC_MESSAGES/default.po +++ b/lang/cs/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/da/LC_MESSAGES/default.po b/lang/da/LC_MESSAGES/default.po index 2170457..2a50071 100644 --- a/lang/da/LC_MESSAGES/default.po +++ b/lang/da/LC_MESSAGES/default.po @@ -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" diff --git a/lang/de/LC_MESSAGES/default.po b/lang/de/LC_MESSAGES/default.po index 8bea16e..e5c3c88 100644 --- a/lang/de/LC_MESSAGES/default.po +++ b/lang/de/LC_MESSAGES/default.po @@ -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" diff --git a/lang/el/LC_MESSAGES/default.po b/lang/el/LC_MESSAGES/default.po index 9118715..eafb2fe 100644 --- a/lang/el/LC_MESSAGES/default.po +++ b/lang/el/LC_MESSAGES/default.po @@ -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 "Χιλιοστά του δευτερολέπτου" diff --git a/lang/en/LC_MESSAGES/default.po b/lang/en/LC_MESSAGES/default.po index cbdef15..eb0843d 100644 --- a/lang/en/LC_MESSAGES/default.po +++ b/lang/en/LC_MESSAGES/default.po @@ -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" diff --git a/lang/eo/LC_MESSAGES/default.po b/lang/eo/LC_MESSAGES/default.po index b71ba56..e0805a9 100644 --- a/lang/eo/LC_MESSAGES/default.po +++ b/lang/eo/LC_MESSAGES/default.po @@ -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" diff --git a/lang/es/LC_MESSAGES/default.po b/lang/es/LC_MESSAGES/default.po index 57baeab..db03c59 100644 --- a/lang/es/LC_MESSAGES/default.po +++ b/lang/es/LC_MESSAGES/default.po @@ -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" diff --git a/lang/et/LC_MESSAGES/default.po b/lang/et/LC_MESSAGES/default.po index 1e9865f..f578b1f 100644 --- a/lang/et/LC_MESSAGES/default.po +++ b/lang/et/LC_MESSAGES/default.po @@ -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" diff --git a/lang/fa/LC_MESSAGES/default.po b/lang/fa/LC_MESSAGES/default.po index e16c524..d4e4e5d 100644 --- a/lang/fa/LC_MESSAGES/default.po +++ b/lang/fa/LC_MESSAGES/default.po @@ -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 "میلی‌ثانیه" diff --git a/lang/fi/LC_MESSAGES/default.po b/lang/fi/LC_MESSAGES/default.po index d966afb..42daf70 100644 --- a/lang/fi/LC_MESSAGES/default.po +++ b/lang/fi/LC_MESSAGES/default.po @@ -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" diff --git a/lang/fr/LC_MESSAGES/default.po b/lang/fr/LC_MESSAGES/default.po index e6b2e63..d1a9c87 100644 --- a/lang/fr/LC_MESSAGES/default.po +++ b/lang/fr/LC_MESSAGES/default.po @@ -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" diff --git a/lang/hi/LC_MESSAGES/default.po b/lang/hi/LC_MESSAGES/default.po index e00fb75..7fd1319 100644 --- a/lang/hi/LC_MESSAGES/default.po +++ b/lang/hi/LC_MESSAGES/default.po @@ -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 "मिलीसेकंड" diff --git a/lang/hr/LC_MESSAGES/default.po b/lang/hr/LC_MESSAGES/default.po index 5207cfb..0e881ab 100644 --- a/lang/hr/LC_MESSAGES/default.po +++ b/lang/hr/LC_MESSAGES/default.po @@ -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" diff --git a/lang/hu/LC_MESSAGES/default.po b/lang/hu/LC_MESSAGES/default.po index 3cbb8eb..f40d775 100644 --- a/lang/hu/LC_MESSAGES/default.po +++ b/lang/hu/LC_MESSAGES/default.po @@ -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" diff --git a/lang/hy/LC_MESSAGES/default.po b/lang/hy/LC_MESSAGES/default.po index ee5c593..0e1492d 100644 --- a/lang/hy/LC_MESSAGES/default.po +++ b/lang/hy/LC_MESSAGES/default.po @@ -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 "Միլիվայրկյաններ" diff --git a/lang/id/LC_MESSAGES/default.po b/lang/id/LC_MESSAGES/default.po index 4f5401c..54e2473 100644 --- a/lang/id/LC_MESSAGES/default.po +++ b/lang/id/LC_MESSAGES/default.po @@ -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" diff --git a/lang/it/LC_MESSAGES/default.po b/lang/it/LC_MESSAGES/default.po index 5260196..0964cb8 100644 --- a/lang/it/LC_MESSAGES/default.po +++ b/lang/it/LC_MESSAGES/default.po @@ -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" diff --git a/lang/iw/LC_MESSAGES/default.po b/lang/iw/LC_MESSAGES/default.po index 0921ce9..eb7c786 100644 --- a/lang/iw/LC_MESSAGES/default.po +++ b/lang/iw/LC_MESSAGES/default.po @@ -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 "אלפיות שניה" diff --git a/lang/ja/LC_MESSAGES/default.po b/lang/ja/LC_MESSAGES/default.po index bac366f..47bf52a 100644 --- a/lang/ja/LC_MESSAGES/default.po +++ b/lang/ja/LC_MESSAGES/default.po @@ -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 "ミリ秒" diff --git a/lang/ko/LC_MESSAGES/default.po b/lang/ko/LC_MESSAGES/default.po index 8df0d01..1ee8d0e 100644 --- a/lang/ko/LC_MESSAGES/default.po +++ b/lang/ko/LC_MESSAGES/default.po @@ -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 "밀리초" diff --git a/lang/lt/LC_MESSAGES/default.po b/lang/lt/LC_MESSAGES/default.po index 6f5b1e2..9f21533 100644 --- a/lang/lt/LC_MESSAGES/default.po +++ b/lang/lt/LC_MESSAGES/default.po @@ -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" diff --git a/lang/lv/LC_MESSAGES/default.po b/lang/lv/LC_MESSAGES/default.po index 69f8bf9..a2ef8c3 100644 --- a/lang/lv/LC_MESSAGES/default.po +++ b/lang/lv/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/nl/LC_MESSAGES/default.po b/lang/nl/LC_MESSAGES/default.po index 14dea78..14b244b 100644 --- a/lang/nl/LC_MESSAGES/default.po +++ b/lang/nl/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/no/LC_MESSAGES/default.po b/lang/no/LC_MESSAGES/default.po index b1167d5..369f472 100644 --- a/lang/no/LC_MESSAGES/default.po +++ b/lang/no/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/pl/LC_MESSAGES/default.po b/lang/pl/LC_MESSAGES/default.po index c43b336..2c48817 100644 --- a/lang/pl/LC_MESSAGES/default.po +++ b/lang/pl/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/pt/LC_MESSAGES/default.po b/lang/pt/LC_MESSAGES/default.po index 55a9058..440abea 100644 --- a/lang/pt/LC_MESSAGES/default.po +++ b/lang/pt/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/ro/LC_MESSAGES/default.po b/lang/ro/LC_MESSAGES/default.po index 6338c2b..a3d3338 100644 --- a/lang/ro/LC_MESSAGES/default.po +++ b/lang/ro/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/ru/LC_MESSAGES/default.po b/lang/ru/LC_MESSAGES/default.po index 152f19e..b90d86d 100644 --- a/lang/ru/LC_MESSAGES/default.po +++ b/lang/ru/LC_MESSAGES/default.po @@ -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 "Миллисекунды" diff --git a/lang/sk/LC_MESSAGES/default.po b/lang/sk/LC_MESSAGES/default.po index 62b6fa8..611db5c 100644 --- a/lang/sk/LC_MESSAGES/default.po +++ b/lang/sk/LC_MESSAGES/default.po @@ -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" diff --git a/lang/sl/LC_MESSAGES/default.po b/lang/sl/LC_MESSAGES/default.po index 8b7d702..1acc1f0 100644 --- a/lang/sl/LC_MESSAGES/default.po +++ b/lang/sl/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/sr/LC_MESSAGES/default.po b/lang/sr/LC_MESSAGES/default.po index 42dddc9..19e953d 100644 --- a/lang/sr/LC_MESSAGES/default.po +++ b/lang/sr/LC_MESSAGES/default.po @@ -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 "Милисекунде" diff --git a/lang/sv/LC_MESSAGES/default.po b/lang/sv/LC_MESSAGES/default.po index ca4e894..cbf0306 100644 --- a/lang/sv/LC_MESSAGES/default.po +++ b/lang/sv/LC_MESSAGES/default.po @@ -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" diff --git a/lang/sw/LC_MESSAGES/default.po b/lang/sw/LC_MESSAGES/default.po index 8482aa6..6834448 100644 --- a/lang/sw/LC_MESSAGES/default.po +++ b/lang/sw/LC_MESSAGES/default.po @@ -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" diff --git a/lang/th/LC_MESSAGES/default.po b/lang/th/LC_MESSAGES/default.po index 3ba894e..4749c83 100644 --- a/lang/th/LC_MESSAGES/default.po +++ b/lang/th/LC_MESSAGES/default.po @@ -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 "มิลลิวินาที" diff --git a/lang/tl/LC_MESSAGES/default.po b/lang/tl/LC_MESSAGES/default.po index a33e188..eab7127 100644 --- a/lang/tl/LC_MESSAGES/default.po +++ b/lang/tl/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/tr/LC_MESSAGES/default.po b/lang/tr/LC_MESSAGES/default.po index e18d06d..aafad53 100644 --- a/lang/tr/LC_MESSAGES/default.po +++ b/lang/tr/LC_MESSAGES/default.po @@ -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" \ No newline at end of file diff --git a/lang/uk/LC_MESSAGES/default.po b/lang/uk/LC_MESSAGES/default.po index f6ecd56..e2fc3ab 100644 --- a/lang/uk/LC_MESSAGES/default.po +++ b/lang/uk/LC_MESSAGES/default.po @@ -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 "Мілісекунди" diff --git a/lang/vi/LC_MESSAGES/default.po b/lang/vi/LC_MESSAGES/default.po index e54522a..a5303ce 100644 --- a/lang/vi/LC_MESSAGES/default.po +++ b/lang/vi/LC_MESSAGES/default.po @@ -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" diff --git a/lang/zh-CN/LC_MESSAGES/default.po b/lang/zh-CN/LC_MESSAGES/default.po index c7921b3..d173139 100644 --- a/lang/zh-CN/LC_MESSAGES/default.po +++ b/lang/zh-CN/LC_MESSAGES/default.po @@ -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 "毫秒" diff --git a/lang/zh-TW/LC_MESSAGES/default.po b/lang/zh-TW/LC_MESSAGES/default.po index 95ba189..117897e 100644 --- a/lang/zh-TW/LC_MESSAGES/default.po +++ b/lang/zh-TW/LC_MESSAGES/default.po @@ -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 "毫秒" diff --git a/main.go b/main.go index a9019e4..12c2381 100755 --- a/main.go +++ b/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) + } } diff --git a/map.go b/map.go index ab3c5a5..4927fc0 100755 --- a/map.go +++ b/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, diff --git a/music-bandcamp.go b/music-bandcamp.go deleted file mode 100644 index 95922ae..0000000 --- a/music-bandcamp.go +++ /dev/null @@ -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 -} diff --git a/music-soundcloud.go b/music-soundcloud.go deleted file mode 100644 index f8a7221..0000000 --- a/music-soundcloud.go +++ /dev/null @@ -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 -} diff --git a/music-spotify.go b/music-spotify.go deleted file mode 100644 index d33e6a3..0000000 --- a/music-spotify.go +++ /dev/null @@ -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 -} diff --git a/music-youtube.go b/music-youtube.go deleted file mode 100644 index 698dc71..0000000 --- a/music-youtube.go +++ /dev/null @@ -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 -} diff --git a/music.go b/music.go deleted file mode 100644 index b92a12b..0000000 --- a/music.go +++ /dev/null @@ -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( -// ``, -// result.IframeSrc, -// )) -// } -// return template.HTML("") -// } diff --git a/node.go b/node.go index aac0804..5fd247a 100644 --- a/node.go +++ b/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) } diff --git a/proxy.go b/proxy.go deleted file mode 100644 index 0f2a26a..0000000 --- a/proxy.go +++ /dev/null @@ -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() -// } -// } -// } diff --git a/run.bat b/run.bat index e1bf056..eb3919d 100755 --- a/run.bat +++ b/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 diff --git a/run.sh b/run.sh index cfdd84a..2aeefad 100755 --- a/run.sh +++ b/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 diff --git a/static/css/style-fonts.css b/static/css/style-fonts.css index 9b9a915..c49eb14 100644 --- a/static/css/style-fonts.css +++ b/static/css/style-fonts.css @@ -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'); } \ No newline at end of file diff --git a/static/css/style-imageloading.css b/static/css/style-imageloading.css deleted file mode 100644 index 5d3b961..0000000 --- a/static/css/style-imageloading.css +++ /dev/null @@ -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 */ - } -} diff --git a/static/css/style-imageviewer.css b/static/css/style-imageviewer.css index f738515..ac6874a 100644 --- a/static/css/style-imageviewer.css +++ b/static/css/style-imageviewer.css @@ -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 { diff --git a/static/css/style-loadingcircle.css b/static/css/style-loadingcircle.css deleted file mode 100644 index ab8884f..0000000 --- a/static/css/style-loadingcircle.css +++ /dev/null @@ -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); - } -} diff --git a/static/css/style-loadingindicator.css b/static/css/style-loadingindicator.css deleted file mode 100644 index 4b58b1e..0000000 --- a/static/css/style-loadingindicator.css +++ /dev/null @@ -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; -} diff --git a/static/css/style-music.css b/static/css/style-music.css deleted file mode 100644 index 6654856..0000000 --- a/static/css/style-music.css +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index a477fa3..e4b1cd6 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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); -} +} \ No newline at end of file diff --git a/static/fonts/MaterialIcons-Round.woff2 b/static/fonts/MaterialIcons-Round.woff2 deleted file mode 100644 index 45ed1fb..0000000 Binary files a/static/fonts/MaterialIcons-Round.woff2 and /dev/null differ diff --git a/static/fonts/material-icons-round-v108-latin-regular.woff2 b/static/fonts/material-icons-round-v108-latin-regular.woff2 new file mode 100644 index 0000000..6f6a973 Binary files /dev/null and b/static/fonts/material-icons-round-v108-latin-regular.woff2 differ diff --git a/static/images/globe.svg b/static/images/globe.svg deleted file mode 100644 index 4837352..0000000 --- a/static/images/globe.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/static/js/dynamicscrolling.js b/static/js/dynamicscrolling.js index 8c9bae2..fb1ed87 100644 --- a/static/js/dynamicscrolling.js +++ b/static/js/dynamicscrolling.js @@ -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; diff --git a/static/js/dynamicscrollingimages.js b/static/js/dynamicscrollingimages.js index ef0da92..6969a53 100644 --- a/static/js/dynamicscrollingimages.js +++ b/static/js/dynamicscrollingimages.js @@ -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); - }); })(); \ No newline at end of file diff --git a/static/js/dynamicscrollingtext.js b/static/js/dynamicscrollingtext.js deleted file mode 100644 index 98811b5..0000000 --- a/static/js/dynamicscrollingtext.js +++ /dev/null @@ -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 = ``; - 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); - // } - // }); -})(); \ No newline at end of file diff --git a/static/js/imageviewer.js b/static/js/imageviewer.js index e04b350..4bd667f 100644 --- a/static/js/imageviewer.js +++ b/static/js/imageviewer.js @@ -28,41 +28,26 @@ document.addEventListener('DOMContentLoaded', function() {

-
-
-
- -
-
- -
-
- -
-
- -
-
+

+ Show source website + Show in fullscreen +

`; 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); - }); -}); \ No newline at end of file + // 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(); + } + } + }); +}); diff --git a/templates/files.html b/templates/files.html index 7a41312..ff35355 100755 --- a/templates/files.html +++ b/templates/files.html @@ -24,9 +24,9 @@
-

{{ translate "settings" }}

+

Settings

- +

Current theme: {{.Theme}}

@@ -65,8 +65,8 @@ src="/static/images/icon.svg" alt="QGato" > -

{{ translate "site_name" }}

-

{{ translate "site_description" }}

+

QGato

+

A open-source private search engine.

@@ -103,10 +103,6 @@
-
- - -
@@ -124,6 +120,8 @@
+

{{ translate "fetched_in" .Fetched }}

+ {{ if .Results }}
@@ -149,8 +147,6 @@
-

{{ translate "fetched_in" .Fetched }}

-
{{ range .Results }}
@@ -158,7 +154,7 @@
{{ translate "error" }}: {{ .Error }}
{{ else }} {{ .URL }} -

{{ .Title }}

+

{{ .Title }}

{{ if .Views }}{{ .Views }} {{ translate "views" }} • {{ end }}{{ .Size }}

{{ translate "seeders" }}: {{ .Seeders }} | {{ translate "leechers" }}: {{ .Leechers }}

{{ end }} diff --git a/templates/forums.html b/templates/forums.html index 330765d..1476537 100755 --- a/templates/forums.html +++ b/templates/forums.html @@ -10,7 +10,6 @@ - @@ -25,9 +24,9 @@
-

{{ translate "settings" }}

+

Settings

- +

Current theme: {{.Theme}}

@@ -66,8 +65,8 @@ src="/static/images/icon.svg" alt="QGato" > -

{{ translate "site_name" }}

-

{{ translate "site_description" }}

+

QGato

+

A open-source private search engine.

@@ -104,10 +103,6 @@
-
- - -
@@ -137,15 +132,13 @@ -

{{ translate "fetched_in" .Fetched }}

-
{{if .Results}} {{range .Results}}
- {{.URL}} -

{{.Header}}

-

{{.Description}}

+ {{.URL}} +

{{.Header}}

+

{{.Description}}


{{end}} @@ -158,8 +151,8 @@
{{ translate "no_more_results" }}
{{end}}
-
- {{ translate "searching_for_new_results" }}... +
+ {{ translate "searching_for_new_results" }}
diff --git a/templates/images.html b/templates/images.html index 06d53a1..fa6df07 100755 --- a/templates/images.html +++ b/templates/images.html @@ -15,10 +15,8 @@ } - - @@ -35,9 +33,9 @@
-

{{ translate "settings" }}

+

Settings

- +

Current theme: {{.Theme}}

@@ -76,8 +74,8 @@ src="/static/images/icon.svg" alt="QGato" > -

{{ translate "site_name" }}

-

{{ translate "site_description" }}

+

QGato

+

A open-source private search engine.

@@ -115,10 +113,6 @@
-
- - -
@@ -152,8 +146,7 @@
- -

{{ translate "fetched_in" .Fetched }}

+ {{ if .Results }}
@@ -211,7 +204,7 @@ {{ end }}
{{ $result.Width }} × {{ $result.Height }}
- {{ $result.Title }} + {{ $result.Title }}
{{ end }} @@ -241,8 +234,8 @@
{{ translate "no_more_results" }}
{{ end }}
-
- {{ translate "searching_for_new_results" }}... +
+ {{ translate "searching_for_new_results" }}
diff --git a/templates/images_only.html b/templates/images_only.html index 633e43e..ae568f7 100644 --- a/templates/images_only.html +++ b/templates/images_only.html @@ -51,7 +51,7 @@ {{ end }}
{{ $result.Width }} × {{ $result.Height }}
- {{ $result.Title }} + {{ $result.Title }}
{{ end }} diff --git a/templates/map.html b/templates/map.html index e6c757a..054f910 100644 --- a/templates/map.html +++ b/templates/map.html @@ -39,9 +39,9 @@
-

{{ translate "settings" }}

+

Settings

- +

Current theme: {{.Theme}}

@@ -80,8 +80,8 @@ src="/static/images/icon.svg" alt="QGato" > -

{{ translate "site_name" }}

-

{{ translate "site_description" }}

+

QGato

+

A open-source private search engine.

@@ -118,10 +118,6 @@
-
- - -
diff --git a/templates/music.html b/templates/music.html deleted file mode 100644 index ce1dd98..0000000 --- a/templates/music.html +++ /dev/null @@ -1,192 +0,0 @@ - - - - - - {{ if .IsThemeDark }} - - {{ end }} - {{ .Query }} - Music Search - {{ translate "site_name" }} - - - - - - - - - - - - - - -
- -
-

{{ translate "settings" }}

-
- -
-

Current theme: {{.Theme}}

-
-
Dark Theme
-
Light Theme
-
-
- - - -
-
-
- - - -
- - - - -
- -
-

-
- - - -
-

-
- - -
-
    -
    - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    -
    - - -
    -
    -
    -
    -
    -

    {{ translate "fetched_in" .Fetched }}

    - -
    - {{if .Results}} - {{range .Results}} -
    - -
    -

    {{.Title}}

    -
    - {{.Artist}} - | - {{.Source}} -
    -
    -
    - {{end}} - {{else if .NoResults}} -
    - {{ translate "no_results_found" .Query }}
    - {{ translate "suggest_rephrase" }} -
    - {{else}} -
    {{ translate "no_more_results" }}
    - {{end}} -
    -
    - {{ translate "searching_for_new_results" }}... -
    -
    -
    - - - -
    -
    -
    - - - - - - \ No newline at end of file diff --git a/templates/privacy.html b/templates/privacy.html index 1cd9029..ca55401 100644 --- a/templates/privacy.html +++ b/templates/privacy.html @@ -24,9 +24,9 @@
    -

    {{ translate "settings" }}

    +

    Settings

    - +

    Current theme: {{.Theme}}

    @@ -63,8 +63,8 @@ src="/static/images/icon.svg" alt="QGato" > -

    {{ translate "site_name" }}

    -

    {{ translate "site_description" }}

    +

    QGato

    +

    A open-source private search engine.

    diff --git a/templates/search.html b/templates/search.html index bd989c9..44445fe 100755 --- a/templates/search.html +++ b/templates/search.html @@ -6,7 +6,7 @@ {{ if .IsThemeDark }} {{ end }} - {{ translate "site_name" }} + {{ translate "site_description" }}