diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml deleted file mode 100644 index c106815..0000000 --- a/.forgejo/workflows/release.yaml +++ /dev/null @@ -1,158 +0,0 @@ -name: QGato CLI Release Build - -on: - workflow_dispatch: {} - -jobs: - build: - runs-on: debian - - steps: - - name: Checkout source - uses: actions/checkout@v4 - - - name: Install latest Go - run: | - echo "📡 Fetching latest Go version string from go.dev..." - GO_FULL=$(curl -sSL https://go.dev/VERSION?m=text | head -n1) - echo "🔍 Latest Go version: $GO_FULL" - - ARCHIVE="${GO_FULL}.linux-amd64.tar.gz" - curl -LO "https://go.dev/dl/${ARCHIVE}" - - mkdir -p "$HOME/.local/go" - tar -C "$HOME/.local/go" --strip-components=1 -xzf "$ARCHIVE" - - echo "$HOME/.local/go/bin" >> $GITHUB_PATH - echo "✅ Go installed locally to $HOME/.local/go" - - - 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 5028c3a..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: 85% chance for Chromium, 15% for Firefox + // Simulated browser usage statistics (in percentages) usageStats := map[string]float64{ - "Firefox": 15.0, - "Chromium": 85.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 4e75b58..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 @@ -142,12 +136,24 @@ func cacheImage(imageURL, imageID string, imageType string) (string, bool, error return cachedImagePath, true, nil } - // Decode image - img, err := safeDecodeImage(contentType, data) - if err != nil { - printErr("Failed to decode favicon: %s [%s] (%v)", imageURL, imageID, err) + // Decode the image based on the content type + var img image.Image + switch contentType { + 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, err + return "", false, fmt.Errorf("unsupported image type: %s", contentType) } if err != nil { @@ -219,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 } } @@ -311,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 } @@ -324,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 @@ -356,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 } } @@ -508,33 +510,6 @@ func cleanupCache() { } } -func safeDecodeImage(contentType string, data []byte) (img image.Image, err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("image decode panic: %v", r) - } - }() - 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: - err = fmt.Errorf("unsupported image type: %s", contentType) - } - return -} - // Serve missing.svg func serveMissingImage(w http.ResponseWriter, r *http.Request) { missingImagePath := filepath.Join("static", "images", "missing.svg") @@ -542,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 afdb4da..ac2902d 100644 --- a/cache.go +++ b/cache.go @@ -11,6 +11,7 @@ import ( // SearchResult is a generic interface for all types of search results. type SearchResult interface{} +// Define various search result types implementing SearchResult interface type TextSearchResult struct { URL string Header string @@ -61,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 @@ -134,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() @@ -159,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() @@ -183,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() } @@ -205,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() @@ -229,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() @@ -295,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: @@ -322,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 06c47bf..75f6b91 100755 --- a/common.go +++ b/common.go @@ -8,7 +8,6 @@ import ( "html/template" mathrand "math/rand" "net/http" - "net/url" "strings" "time" ) @@ -29,59 +28,14 @@ var ( } return string(jsonBytes), nil }, - "formatShortDate": func(date string) string { - t, _ := time.Parse("2006-01-02", date) - // return t.Format("Mon") // e.g. "Sat" - return t.Format("2.1.") // e.g. "29.6." - }, - "weatherIcon": func(cur interface{}) string { - switch c := cur.(type) { - case map[string]interface{}: - if cond, ok := c["Condition"].(string); ok { - return iconForCond(cond) - } - case WeatherCurrent: - return iconForCond(c.Condition) - case *WeatherCurrent: - return iconForCond(c.Condition) - } - return "🌈" - }, } ) -func iconForCond(cond string) string { - switch cond { - case "Clear": - return "☀️" - case "Partly cloudy": - return "⛅" - case "Cloudy": - return "☁️" - case "Rain": - return "🌧️" - case "Snow": - return "❄️" - case "Thunderstorm": - return "⛈️" - case "Fog": - return "🌫️" - default: - return "🌈" - } -} - type SearchEngine struct { Name string 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 @@ -153,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 9ceaa06..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,42 +22,24 @@ type CacheConfig struct { Path string } -type MetaSearchConfig struct { - Text []string - Image []string - Files []string - Video []string -} - type Config struct { - Port int - NodeID string - Nodes []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 @@ -64,54 +48,20 @@ type Config struct { var defaultConfig = Config{ Port: 5000, Domain: "localhost", - Nodes: []string{}, + 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", "Nyaa"}, - - // 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 @@ -266,6 +216,12 @@ func createConfig() error { config = defaultConfig } + // Generate AuthCode if missing + if config.AuthCode == "" { + config.AuthCode = generateStrongRandomString(64) + printMessage("Generated connection code: %s\n", config.AuthCode) + } + saveConfig(config) printInfo("Configuration saved successfully.") return nil @@ -280,59 +236,26 @@ func saveConfig(config Config) { sec.Key("Domain").SetValue(config.Domain) sec.Key("LogLevel").SetValue(strconv.Itoa(config.LogLevel)) - // Nodes section - nodesSec := cfg.Section("Nodes") - nodesSec.Key("NodeID").SetValue(config.NodeID) - nodesSec.Key("Nodes").SetValue(strings.Join(config.Nodes, ",")) + // Peers section + peersSec := cfg.Section("Peers") + peersSec.Key("AuthCode").SetValue(config.AuthCode) + peersSec.Key("PeerID").SetValue(config.PeerID) + peersSec.Key("Peers").SetValue(strings.Join(config.Peers, ",")) // Features section - if config.NodesEnabled != defaultConfig.NodesEnabled || - config.MetaSearchEnabled != defaultConfig.MetaSearchEnabled || - config.IndexerEnabled != defaultConfig.IndexerEnabled || - config.WebsiteEnabled != defaultConfig.WebsiteEnabled || - config.MetaProxyEnabled != defaultConfig.MetaProxyEnabled || - config.CrawlerProxyEnabled != defaultConfig.CrawlerProxyEnabled { - - featuresSec := cfg.Section("Features") - setBoolIfChanged(featuresSec, "Nodes", config.NodesEnabled, defaultConfig.NodesEnabled) - setBoolIfChanged(featuresSec, "Crawler", config.MetaSearchEnabled, defaultConfig.MetaSearchEnabled) - setBoolIfChanged(featuresSec, "Indexer", config.IndexerEnabled, defaultConfig.IndexerEnabled) - setBoolIfChanged(featuresSec, "Website", config.WebsiteEnabled, defaultConfig.WebsiteEnabled) - setBoolIfChanged(featuresSec, "MetaProxy", config.MetaProxyEnabled, defaultConfig.MetaProxyEnabled) - setBoolIfChanged(featuresSec, "CrawlerProxy", config.CrawlerProxyEnabled, defaultConfig.CrawlerProxyEnabled) - } - - // Proxies section - proxiesSec := cfg.Section("Proxies") - proxiesSec.Key("MetaProxyStrict").SetValue(strconv.FormatBool(config.MetaProxyStrict)) - proxiesSec.Key("MetaProxies").SetValue(strings.Join(config.MetaProxies, ",")) - setBoolIfChanged(proxiesSec, "CrawlerProxyStrict", config.CrawlerProxyStrict, defaultConfig.CrawlerProxyStrict) - setSliceIfChanged(proxiesSec, "CrawlerProxies", config.CrawlerProxies, defaultConfig.CrawlerProxies) - proxiesSec.Key("MetaProxyRetry").SetValue(strconv.Itoa(config.MetaProxyRetry)) - setIntIfChanged(proxiesSec, "CrawlerProxyRetry", config.CrawlerProxyRetry, defaultConfig.CrawlerProxyRetry) - - // MetaSearch section - metaSec := cfg.Section("MetaSearch") - 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, ",")) + featuresSec := cfg.Section("Features") + featuresSec.Key("Nodes").SetValue(strconv.FormatBool(config.NodesEnabled)) + featuresSec.Key("Crawler").SetValue(strconv.FormatBool(config.CrawlerEnabled)) + featuresSec.Key("Indexer").SetValue(strconv.FormatBool(config.IndexerEnabled)) + featuresSec.Key("Website").SetValue(strconv.FormatBool(config.WebsiteEnabled)) // Indexer section - if config.ConcurrentStandardCrawlers != defaultConfig.ConcurrentStandardCrawlers || - config.ConcurrentChromeCrawlers != defaultConfig.ConcurrentChromeCrawlers || - config.CrawlingInterval != defaultConfig.CrawlingInterval || - config.MaxPagesPerDomain != defaultConfig.MaxPagesPerDomain || - config.IndexBatchSize != defaultConfig.IndexBatchSize { - - indexerSec := cfg.Section("Indexer") - setIntIfChanged(indexerSec, "ConcurrentStandardCrawlers", config.ConcurrentStandardCrawlers, defaultConfig.ConcurrentStandardCrawlers) - setIntIfChanged(indexerSec, "ConcurrentChromeCrawlers", config.ConcurrentChromeCrawlers, defaultConfig.ConcurrentChromeCrawlers) - setIfChanged(indexerSec, "CrawlingInterval", config.CrawlingInterval.String(), defaultConfig.CrawlingInterval.String()) - setIntIfChanged(indexerSec, "MaxPagesPerDomain", config.MaxPagesPerDomain, defaultConfig.MaxPagesPerDomain) - setIntIfChanged(indexerSec, "IndexBatchSize", config.IndexBatchSize, defaultConfig.IndexBatchSize) - } + indexerSec := cfg.Section("Indexer") + indexerSec.Key("ConcurrentStandardCrawlers").SetValue(strconv.Itoa(config.ConcurrentStandardCrawlers)) + 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)) // DriveCache section driveSec := cfg.Section("DriveCache") @@ -363,48 +286,18 @@ func loadConfig() Config { domain := getConfigValueString(cfg.Section("Server").Key("Domain"), defaultConfig.Domain) logLevel := getConfigValue(cfg.Section("Server").Key("LogLevel"), defaultConfig.LogLevel, strconv.Atoi) - // Nodes - nodeID := getConfigValueString(cfg.Section("Nodes").Key("NodeID"), defaultConfig.NodeID) - nodes := strings.Split(getConfigValueString(cfg.Section("Nodes").Key("Nodes"), ""), ",") + // Peers + authCode := getConfigValueString(cfg.Section("Peers").Key("AuthCode"), defaultConfig.AuthCode) + peers := strings.Split(getConfigValueString(cfg.Section("Peers").Key("Peers"), ""), ",") // 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) - // Nodes - 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("MetaSearch").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, ",")), ",") - - // Load default values for MetaSearch if they are empty - if isEmptyList(textList) { - textList = defaultConfig.MetaSearch.Text - } - if isEmptyList(imageList) { - imageList = defaultConfig.MetaSearch.Image - } - if isEmptyList(filesList) { - filesList = defaultConfig.MetaSearch.Files - } - if isEmptyList(videoList) { - videoList = defaultConfig.MetaSearch.Video - } // Indexing concurrentStandardCrawlers := getConfigValue(cfg.Section("Indexer").Key("ConcurrentStandardCrawlers"), defaultConfig.ConcurrentStandardCrawlers, strconv.Atoi) concurrentChromeCrawlers := getConfigValue(cfg.Section("Indexer").Key("ConcurrentChromeCrawlers"), defaultConfig.ConcurrentChromeCrawlers, strconv.Atoi) @@ -429,34 +322,19 @@ func loadConfig() Config { Port: port, Domain: domain, LogLevel: logLevel, - NodeID: nodeID, - Nodes: nodes, + 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, @@ -469,34 +347,6 @@ func loadConfig() Config { } } -func isEmptyList(list []string) bool { - return len(list) == 0 || (len(list) == 1 && strings.TrimSpace(list[0]) == "") -} - -func setIfChanged(sec *ini.Section, key string, value string, defaultValue string) { - if value != defaultValue { - sec.Key(key).SetValue(value) - } -} - -func setBoolIfChanged(sec *ini.Section, key string, value bool, defaultValue bool) { - if value != defaultValue { - sec.Key(key).SetValue(strconv.FormatBool(value)) - } -} - -func setIntIfChanged(sec *ini.Section, key string, value int, defaultValue int) { - if value != defaultValue { - sec.Key(key).SetValue(strconv.Itoa(value)) - } -} - -func setSliceIfChanged(sec *ini.Section, key string, value, defaultValue []string) { - if strings.Join(value, ",") != strings.Join(defaultValue, ",") { - sec.Key(key).SetValue(strings.Join(value, ",")) - } -} - // getConfigValue retrieves a configuration value or returns a default value from defaultConfig. func getConfigValue[T any](key *ini.Key, defaultValue T, parseFunc func(string) (T, error)) T { if key == nil || key.String() == "" { @@ -582,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 1297d27..4ce8b9d 100644 --- a/crawler-extraction.go +++ b/crawler-extraction.go @@ -1,6 +1,3 @@ -//go:build experimental -// +build experimental - package main import ( @@ -35,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 @@ -64,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) @@ -102,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 @@ -211,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) @@ -219,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/crawler-visited.go b/crawler-visited.go index ddabb98..bfa1af9 100644 --- a/crawler-visited.go +++ b/crawler-visited.go @@ -1,6 +1,3 @@ -//go:build experimental -// +build experimental - package main import ( diff --git a/crawler.go b/crawler.go index 795b189..8caa073 100644 --- a/crawler.go +++ b/crawler.go @@ -1,6 +1,3 @@ -//go:build experimental -// +build experimental - package main import ( 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 cd08682..0000000 --- a/favicon.go +++ /dev/null @@ -1,544 +0,0 @@ -package main - -import ( - "crypto/md5" - "crypto/tls" - "encoding/base64" - "encoding/hex" - "fmt" - "image" - "io" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "github.com/chai2010/webp" - "golang.org/x/image/draw" - "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", - } -) - -// 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 - printDebug("Downloading favicon ID: %s\n", 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 - img, err := safeDecodeImage(contentType, data) - if err != nil { - printErr("Failed to decode favicon: %s [%s] (%v)", imageURL, imageID, err) - 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-nyaa.go b/files-nyaa.go deleted file mode 100644 index db9f04a..0000000 --- a/files-nyaa.go +++ /dev/null @@ -1,107 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/PuerkitoBio/goquery" -) - -const NYAA_DOMAIN = "nyaa.si" - -type Nyaa struct{} - -func NewNyaa() *Nyaa { - return &Nyaa{} -} - -func (n *Nyaa) Name() string { - return "nyaa" -} - -func (n *Nyaa) getCategoryCode(cat string) string { - switch cat { - case "all": - return "" - case "anime": - return "&c=1_0" - case "music": - return "&c=2_0" - case "game": - return "&c=6_2" - case "software": - return "&c=6_1" - default: - return "ignore" - } -} - -func (n *Nyaa) Search(query string, category string) ([]TorrentResult, error) { - categoryCode := n.getCategoryCode(category) - if categoryCode == "ignore" { - return []TorrentResult{}, nil - } - - searchURL := fmt.Sprintf("https://%s/?f=0&q=%s%s", NYAA_DOMAIN, url.QueryEscape(query), categoryCode) - - userAgent, err := GetUserAgent("files-nyaa") - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", searchURL, nil) - if err != nil { - return nil, err - } - req.Header.Set("User-Agent", userAgent) - - resp, err := DoMetaProxyRequest(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) - } - - doc, err := goquery.NewDocumentFromReader(resp.Body) - if err != nil { - return nil, err - } - - var results []TorrentResult - doc.Find(".default, .success, .danger").Each(func(i int, s *goquery.Selection) { - tds := s.Find("td") - if tds.Length() < 7 { - return - } - - title := tds.Eq(1).Find("a").Last().Text() - magnet, _ := tds.Eq(2).Find("a").Last().Attr("href") - sizeStr := strings.TrimSpace(tds.Eq(3).Text()) - byteSize := parseSize(sizeStr) - - seeders := parseInt(tds.Eq(5).Text()) - leechers := parseInt(tds.Eq(6).Text()) - - results = append(results, TorrentResult{ - URL: "https://" + NYAA_DOMAIN, - Title: title, - Magnet: applyTrackers(magnet), - Size: formatSize(byteSize), - Seeders: seeders, - Leechers: leechers, - Views: 0, - }) - }) - - // Reverse the results slice, so It's from newest to oldest, but the orders is still kinda random - for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 { - results[i], results[j] = results[j], results[i] - } - - return results, 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 1c71940..d0c1ff1 100755 --- a/files.go +++ b/files.go @@ -28,25 +28,13 @@ var ( rutor TorrentSite ) -func initFileEngines() { +var fileResultsChan = make(chan []TorrentResult) - 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) { @@ -64,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, @@ -100,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)) @@ -109,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)) @@ -129,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 { @@ -152,6 +140,11 @@ func fetchFileResults(query, safe, lang string, page int) []TorrentResult { } } + if len(results) == 0 { + printWarn("No file results found for query: %s, trying other nodes", query) + results = tryOtherNodesForFileSearch(query, safe, lang, page, []string{hostID}) + } + return results } @@ -174,34 +167,33 @@ func parseSize(sizeStr string) int64 { return 0 } - re := regexp.MustCompile(`(?i)([\d.]+)\s*(K?M?G?T?i?B)`) + // Use regex to extract numeric value and unit separately + re := regexp.MustCompile(`(?i)([\d.]+)\s*([KMGT]?B)`) matches := re.FindStringSubmatch(sizeStr) if len(matches) < 3 { printWarn("Error parsing size: invalid format %s", sizeStr) return 0 } - numStr := matches[1] + sizeStr = matches[1] unit := strings.ToUpper(matches[2]) var multiplier int64 = 1 switch unit { - case "B": - multiplier = 1 - case "KB", "KIB": + case "KB": multiplier = 1024 - case "MB", "MIB": + case "MB": multiplier = 1024 * 1024 - case "GB", "GIB": + case "GB": multiplier = 1024 * 1024 * 1024 - case "TB", "TIB": + case "TB": multiplier = 1024 * 1024 * 1024 * 1024 default: printWarn("Unknown unit: %s", unit) return 0 } - size, err := strconv.ParseFloat(numStr, 64) + size, err := strconv.ParseFloat(sizeStr, 64) if err != nil { printWarn("Error parsing size: %v", err) return 0 @@ -227,16 +219,16 @@ func applyTrackers(magnetLink string) string { } func formatSize(size int64) string { - const unit = 1024 - if size < unit { - return fmt.Sprintf("%d B", size) + if size >= 1024*1024*1024*1024 { + return fmt.Sprintf("%.2f TB", float64(size)/(1024*1024*1024*1024)) + } else if size >= 1024*1024*1024 { + return fmt.Sprintf("%.2f GB", float64(size)/(1024*1024*1024)) + } else if size >= 1024*1024 { + return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024)) + } else if size >= 1024 { + return fmt.Sprintf("%.2f KB", float64(size)/1024) } - div, exp := unit, 0 - for n := size / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %siB", float64(size)/float64(div), []string{"K", "M", "G", "T", "P", "E"}[exp]) + return fmt.Sprintf("%d B", size) } func sanitizeFileName(name string) string { @@ -246,3 +238,12 @@ func sanitizeFileName(name string) string { sanitized = regexp.MustCompile(`[^a-zA-Z0-9\-\(\)]`).ReplaceAllString(sanitized, "") return sanitized } + +func contains(slice []string, item string) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} diff --git a/forums.go b/forums.go index a32bb66..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, @@ -135,15 +131,6 @@ func handleForumsSearch(w http.ResponseWriter, settings UserSettings, query stri renderTemplate(w, "forums.html", data) } -func fetchForumResults(query, safe, lang string, page int) []ForumSearchResult { - results, err := PerformRedditSearch(query, safe, page) - if err != nil { - printWarn("Failed to fetch forum results: %v", err) - return nil - } - return results -} - func getForumResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string, page int) []ForumSearchResult { cacheChan := make(chan []SearchResult) var combinedResults []ForumSearchResult @@ -163,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)) @@ -177,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/ia-calc.go b/ia-calc.go deleted file mode 100644 index 1e629e8..0000000 --- a/ia-calc.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "fmt" - "regexp" - "strconv" - "strings" -) - -// Enhanced math expression parser -func parseMathExpression(query string) (string, bool) { - // Clean and normalize the expression - query = strings.ReplaceAll(query, " ", "") - query = strings.ReplaceAll(query, ",", "") - - // Regex to match valid math expressions - mathRegex := regexp.MustCompile(`^\d+(\.\d+)?([\+\-\*/\^]\d+(\.\d+)?)+$`) - if !mathRegex.MatchString(query) { - return "", false - } - - // Operator precedence handling - operators := []struct { - symbol string - apply func(float64, float64) float64 - }{ - {"^", func(a, b float64) float64 { - result := 1.0 - for i := 0; i < int(b); i++ { - result *= a - } - return result - }}, - {"*", func(a, b float64) float64 { return a * b }}, - {"/", func(a, b float64) float64 { return a / b }}, - {"+", func(a, b float64) float64 { return a + b }}, - {"-", func(a, b float64) float64 { return a - b }}, - } - - // Parse numbers and operators - var tokens []interface{} - current := "" - for _, char := range query { - if char >= '0' && char <= '9' || char == '.' { - current += string(char) - } else { - if current != "" { - num, _ := strconv.ParseFloat(current, 64) - tokens = append(tokens, num) - current = "" - } - tokens = append(tokens, string(char)) - } - } - if current != "" { - num, _ := strconv.ParseFloat(current, 64) - tokens = append(tokens, num) - } - - // Evaluate expression with operator precedence - for _, op := range operators { - for i := 1; i < len(tokens)-1; i += 2 { - if operator, ok := tokens[i].(string); ok && operator == op.symbol { - left := tokens[i-1].(float64) - right := tokens[i+1].(float64) - result := op.apply(left, right) - - // Update tokens - tokens = append(tokens[:i-1], tokens[i+2:]...) - tokens = append(tokens[:i-1], append([]interface{}{result}, tokens[i-1:]...)...) - i -= 2 // Adjust index after modification - } - } - } - - // Format result - result := tokens[0].(float64) - if result == float64(int(result)) { - return fmt.Sprintf("%d", int(result)), true - } - return fmt.Sprintf("%.2f", result), true -} diff --git a/ia-currency.go b/ia-currency.go deleted file mode 100644 index 8618055..0000000 --- a/ia-currency.go +++ /dev/null @@ -1,418 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "regexp" - "strconv" - "strings" - "sync" - "time" -) - -// ExchangeRateCache holds currency rates with automatic refresh -var ( - exchangeRates = make(map[string]float64) - nextUpdateTime time.Time - lastUpdateTime time.Time - exchangeCacheMutex sync.RWMutex - allCurrencies []string -) - -// CurrencyAPIResponse structure for exchange rate API -type CurrencyAPIResponse struct { - Rates map[string]float64 `json:"rates"` -} - -var primaryURL = "https://open.er-api.com/v6/latest/USD" -var backupURL = "https://api.frankfurter.app/latest?base=USD" - -func fetchRates(url string) ([]byte, error) { - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - return io.ReadAll(resp.Body) -} - -func UpdateExchangeRates() error { - exchangeCacheMutex.Lock() - defer exchangeCacheMutex.Unlock() - - var ( - rates map[string]float64 - nextTime time.Time - fallback bool - ) - - // Try primary API - body, err := fetchRates(primaryURL) - if err == nil { - var res struct { - Result string `json:"result"` - Rates map[string]float64 `json:"rates"` - TimeNextUpdateUnix int64 `json:"time_next_update_unix"` - } - if err := json.Unmarshal(body, &res); err == nil && res.Result == "success" && len(res.Rates) > 0 { - rates = res.Rates - nextTime = time.Unix(res.TimeNextUpdateUnix, 0) - } else { - printWarn("Primary API response invalid or empty, falling back to backup") - fallback = true - } - } else { - printWarn("Primary API fetch failed: %v", err) - fallback = true - } - - // Try backup API if needed - if fallback { - body, err := fetchRates(backupURL) - if err != nil { - return fmt.Errorf("both rate fetches failed: %v", err) - } - - var res struct { - Base string `json:"base"` - Date string `json:"date"` - Rates map[string]float64 `json:"rates"` - } - if err := json.Unmarshal(body, &res); err != nil { - return fmt.Errorf("backup API response unmarshal failed: %v", err) - } - if len(res.Rates) == 0 { - return fmt.Errorf("backup API returned empty rates") - } - - rates = res.Rates - nextTime = time.Now().Add(6 * time.Hour) - } - - // Finalize - exchangeRates = rates - nextUpdateTime = nextTime - lastUpdateTime = time.Now() - - allCurrencies = make([]string, 0, len(exchangeRates)) - for c := range exchangeRates { - allCurrencies = append(allCurrencies, c) - } - - printDebug("Updated currency rates: %d currencies cached", len(allCurrencies)) - printDebug("Next currency update at: %s", nextUpdateTime.Format(time.RFC1123)) - return nil -} - -// PrecacheAllCurrencyPairs pre-caches conversion rates for all currency pairs -func PrecacheAllCurrencyPairs() { - exchangeCacheMutex.RLock() - defer exchangeCacheMutex.RUnlock() - - if len(exchangeRates) == 0 { - printWarn("Skipping precache: no currency rates available") - return - } - - printDebug("Precaching all currency pairs (%d total)", len(exchangeRates)) - - for from := range exchangeRates { - for to := range exchangeRates { - if from == to { - continue - } - // Cache the cross-rate - GetExchangeRate(from, to) - } - } - - printDebug("All currency pairs precached") -} - -// GetExchangeRate gets the current exchange rate with caching -func GetExchangeRate(from, to string) (float64, bool) { - // Auto-update cache if expired - if time.Now().After(nextUpdateTime) { - // Avoid excessive updates within 1 min - if time.Since(lastUpdateTime) > time.Minute { - err := UpdateExchangeRates() - if err != nil { - printWarn("Currency update failed: %v", err) - nextUpdateTime = time.Now().Add(5 * time.Minute) - } - } - } - - exchangeCacheMutex.RLock() - defer exchangeCacheMutex.RUnlock() - - // Handle same currency - if from == to { - return 1, true - } - - // Convert via USD if direct rate not available - fromRate, fromExists := exchangeRates[from] - toRate, toExists := exchangeRates[to] - - if !fromExists || !toExists { - return 0, false - } - - // Calculate cross rate: (1 USD / fromRate) * toRate - return toRate / fromRate, true -} - -// ParseCurrencyConversion detects and processes currency conversion queries -func ParseCurrencyConversion(query string) (float64, string, string, bool) { - // Main conversion phrases - conversionPhrases := []string{ - // Universal/math - "➞", "→", "⇒", ">", "->", "=", "≈", "~", ":", "≡", - // English - "to", "in", "into", "as", "equals", "equal to", "equals to", "is", "becomes", "be", "makes", "converted to", "convert to", "convert into", "converted into", - "exchange for", "exchanged for", "value in", "as currency", "convert", "equivalent to", "same as", "is equal to", ">", "gives", "makes", "result is", "returns", "will be", "equals:", "is equivalent to", "≈", "~", ":", - // German (DE) - "auf", "in", "zu", "umrechnen in", "umrechnen zu", "als", "gleich", "ist", "ist gleich", "umwandeln in", "wird zu", "ergibt", "macht", "ist", "resultiert in", "gleichwertig mit", - // Spanish (ES) - "en", "a", "como", "igual a", "es", "es igual a", "es igual", "convertir a", "cambiar a", "valor en", "convierte en", "devuelve", "será", "equivale a", "es equivalente a", - // French (FR) - "vers", "en", "comme", "égal à", "est", "c'est", "convertir en", "changer en", "valeur en", "équivaut à", "sera", "fait", "rend", "est égal à", "équivalent à", - // Italian (IT) - "a", "in", "come", "uguale a", "è", "convertire in", "cambiare in", "valore in", "sarà", "fa", "equivale a", "è uguale a", - // Portuguese (PT/BR) - "para", "em", "como", "igual a", "é", "converter para", "trocar por", "valor em", "converte em", "vai ser", "faz", "equivale a", "é igual a", "é equivalente a", - // Dutch (NL) - "naar", "in", "als", "is gelijk aan", "is", "wordt", "omzetten naar", "waarde in", "gelijk aan", "is hetzelfde als", - // Czech (CS) - "na", "do", "jako", "rovná se", "je", "převést na", "výměna za", "hodnota v", "přepočet", "bude", "rovná", "je to", "je rovno", "je stejné jako", - // Slovak (SK) - "na", "do", "ako", "rovná sa", "je", "previesť na", "výměna za", "hodnota v", "prerátať", "bude", "rovná", "je to", "je rovné", "je rovnaké ako", - // Polish (PL) - "na", "w", "jako", "równa się", "jest", "przelicz na", "wymień na", "wartość w", "przelicza się na", "będzie", "to jest", "jest równy", "jest taki sam jak", - // Russian (RU) - "на", "в", "как", "равно", "есть", "конвертировать в", "обменять на", "значение в", "равняется", "будет", "это", "такое же как", - // Ukrainian (UA) - "на", "у", "як", "дорівнює", "є", "конвертувати у", "обміняти на", "значення в", "буде", "це", "таке саме як", - // Croatian / Serbian / Bosnian / Slovenian (HR/SR/BS/SL) - "na", "u", "za", "kao", "jednako", "je", "pretvori u", "zamijeniti za", "vrijednost u", "preračunaj u", "biti", "to je", "jednako kao", "je isto kao", - "v", "kot", "je enako", "pretvoriti v", "zamenjati za", "vrednost v", "je isto kao", "je enakovredno", - // Bulgarian (BG) - "на", "в", "като", "равно на", "е", "преобразувай в", "обмени на", "стойност в", "ще бъде", "това е", "равностойно на", - // Turkish (TR) - "için", "olarak", "eşittir", "bu", "dönüştür to", "değiştir to", "değer olarak", "olur", "eşit", "bu olur", "aynı olarak", - // Greek (EL) - "σε", "ως", "ίσον", "είναι", "μετατροπή σε", "ανταλλαγή με", "τιμή σε", "θα είναι", "αυτό είναι", "ισοδυναμεί με", "ίσο με", - // Chinese (Simplified and Traditional, ZH) - "到", "变为", "換成", "转换为", "等于", "等於", "是", "为", "結果是", "相等於", "等同於", "一樣", - // Japanese (JA) - "に", "として", "等しい", "は", "に変換", "に交換", "の値", "は", "結果は", "となる", "同じ", "等価", "等しく", - // Korean (KO) - "으로", "같이", "같다", "이다", "로 변환", "교환하다", "값", "이 된다", "와 같다", "같음", "동일하다", - // Arabic (AR) - "إلى", "الى", "في", "كـ", "يساوي", "هو", "تحويل إلى", "قيمة في", "يصبح", "يساوي نفس", "تعادل", "تساوي", - // Hebrew (HE) - "ל", "ב", "בתור", "שווה ל", "הוא", "המר ל", "ערך ב", "יהיה", "אותו הדבר כמו", "זהה ל", - // Romanian (RO) - "la", "în", "ca", "egal cu", "este", "converti la", "schimbă în", "valoare în", "va fi", "este egal cu", - // Hungarian (HU) - "ra", "re", "ba", "be", "mint", "egyenlő", "az", "átvált", "értéke", "lesz", "ugyanaz mint", - // Swedish (SE) - "till", "i", "som", "är", "är lika med", "omvandla till", "värde i", "blir", "är samma som", - // Danish (DK) - "til", "i", "som", "er", "er lig med", "konverter til", "værdi i", "bliver", "er det samme som", - // Norwegian (NO) - "til", "i", "som", "er", "er lik", "konverter til", "verdi i", "blir", "er det samme som", - // Finnish (FI) - "ksi", "in", "kuin", "on", "on yhtä kuin", "muunna", "arvo", "tulee olemaan", "sama kuin", - // Estonian (EE) - "ks", "sisse", "nagu", "on", "on võrdne", "teisendada", "väärtus", "saab olema", "sama mis", - // Latvian (LV) - "uz", "iekš", "kā", "ir", "ir vienāds ar", "konvertēt uz", "vērtība", "būs", "tāpat kā", - // Lithuanian (LT) - "į", "kaip", "yra", "yra lygus", "konvertuoti į", "vertė", "bus", "tas pats kaip", - // Persian (FA) - "به", "در", "مثل", "برابر با", "است", "تبدیل به", "ارزش در", "خواهد بود", "همانند", - // Hindi (HI) - "को", "में", "के रूप में", "बराबर", "है", "में बदलें", "मूल्य में", "होगा", "के समान", - // Thai (TH) - "ไปที่", "ใน", "เป็น", "เท่ากับ", "คือ", "แปลงเป็น", "ค่าใน", "จะเป็น", "เท่ากัน", - // Indonesian (ID) - "ke", "dalam", "sebagai", "sama dengan", "adalah", "konversi ke", "nilai dalam", "akan menjadi", "sama dengan", - // Vietnamese (VI) - "thành", "trong", "là", "bằng", "là", "chuyển đổi thành", "giá trị trong", "sẽ là", "tương đương với", - // Malay (MS) - "kepada", "dalam", "sebagai", "sama dengan", "ialah", "tukar ke", "nilai dalam", "akan jadi", "setara dengan", - // Filipino/Tagalog (TL) - "sa", "sa loob ng", "bilang", "katumbas ng", "ay", "i-convert sa", "halaga sa", "magiging", "pareho sa", - } - - // Build the OR group for all currency conversion phrases to use in the regex pattern - var orGroup strings.Builder - for i, phrase := range conversionPhrases { - if i > 0 { - orGroup.WriteString("|") - } - // escape for regex with special symbols: - orGroup.WriteString(regexp.QuoteMeta(phrase)) - } - regexPattern := fmt.Sprintf(`(?i)([\d,]+(?:\.\d+)?)\s*([^\d,]+?)\s+(?:%s)\s+([^\d,]+)`, orGroup.String()) - re := regexp.MustCompile(regexPattern) - matches := re.FindStringSubmatch(query) - if len(matches) < 4 { - return 0, "", "", false - } - - // Clean and parse amount - amountStr := strings.ReplaceAll(matches[1], ",", "") - amount, err := strconv.ParseFloat(amountStr, 64) - if err != nil { - return 0, "", "", false - } - - // Normalize currency symbols - currencyMap := map[string]string{ - // Major Global Currencies - "$": "USD", "usd": "USD", "dollar": "USD", "dollars": "USD", "buck": "USD", "bucks": "USD", "us dollar": "USD", "american dollar": "USD", "freedom units": "USD", - "€": "EUR", "eur": "EUR", "euro": "EUR", "euros": "EUR", - "£": "GBP", "gbp": "GBP", "pound": "GBP", "pounds": "GBP", "sterling": "GBP", "quid": "GBP", "pound sterling": "GBP", - "¥": "JPY", "jpy": "JPY", "yen": "JPY", "cn¥": "CNY", // Handle ¥ ambiguity with CN¥ for Chinese Yuan - "₩": "KRW", "krw": "KRW", "won": "KRW", "korean won": "KRW", - "₹": "INR", "inr": "INR", "rupee": "INR", "rupees": "INR", "indian rupee": "INR", - "₽": "RUB", "rub": "RUB", "ruble": "RUB", "rubles": "RUB", "russian ruble": "RUB", - - // Americas - "c$": "CAD", "cad": "CAD", "canadian dollar": "CAD", "loonie": "CAD", - "a$": "AUD", "aud": "AUD", "australian dollar": "AUD", "aussie dollar": "AUD", - "nz$": "NZD", "nzd": "NZD", "new zealand dollar": "NZD", "kiwi": "NZD", "kiwi dollar": "NZD", - "r$": "BRL", "brl": "BRL", "real": "BRL", "reais": "BRL", "brazilian real": "BRL", - "mx$": "MXN", "mxn": "MXN", "mexican peso": "MXN", "mexican pesos": "MXN", - "col$": "COP", "cop": "COP", "colombian peso": "COP", - "s/": "PEN", "pen": "PEN", "sol": "PEN", "soles": "PEN", "peruvian sol": "PEN", - "clp$": "CLP", "clp": "CLP", "chilean peso": "CLP", - "arg$": "ARS", "ars": "ARS", "argentine peso": "ARS", - - // Europe & CIS - "chf": "CHF", "fr": "CHF", "swiss franc": "CHF", "franc suisse": "CHF", - "sek": "SEK", "kr": "SEK", "swedish krona": "SEK", "swedish kronor": "SEK", - "nok": "NOK", "norwegian krone": "NOK", "norwegian kroner": "NOK", - "dkk": "DKK", "danish krone": "DKK", "danish kroner": "DKK", - "zł": "PLN", "pln": "PLN", "zloty": "PLN", "polish zloty": "PLN", - "tl": "TRY", "try": "TRY", "turkish lira": "TRY", "türk lirası": "TRY", "₺": "TRY", - "huf": "HUF", "ft": "HUF", "forint": "HUF", "hungarian forint": "HUF", - "czk": "CZK", "kč": "CZK", "czech koruna": "CZK", - "ron": "RON", "lei": "RON", "romanian leu": "RON", - "bgn": "BGN", "лв": "BGN", "bulgarian lev": "BGN", - "uah": "UAH", "₴": "UAH", "hryvnia": "UAH", "ukrainian hryvnia": "UAH", - "kzt": "KZT", "₸": "KZT", "tenge": "KZT", "kazakhstani tenge": "KZT", - - // Asia/Pacific - "cny": "CNY", "rmb": "CNY", "yuan": "CNY", "renminbi": "CNY", "chinese yuan": "CNY", - "hk$": "HKD", "hkd": "HKD", "hong kong dollar": "HKD", - "s$": "SGD", "sgd": "SGD", "singapore dollar": "SGD", - "nt$": "TWD", "twd": "TWD", "taiwan dollar": "TWD", "new taiwan dollar": "TWD", - "฿": "THB", "thb": "THB", "baht": "THB", "thai baht": "THB", - "rp": "IDR", "idr": "IDR", "rupiah": "IDR", "indonesian rupiah": "IDR", - "₱": "PHP", "php": "PHP", "philippine peso": "PHP", - "rm": "MYR", "myr": "MYR", "ringgit": "MYR", "malaysian ringgit": "MYR", - "₫": "VND", "vnd": "VND", "dong": "VND", "vietnamese dong": "VND", - "₭": "LAK", "lak": "LAK", "kip": "LAK", "lao kip": "LAK", - "៛": "KHR", "khr": "KHR", "riel": "KHR", "cambodian riel": "KHR", - - // Middle East & Africa - "₪": "ILS", "ils": "ILS", "shekel": "ILS", "new israeli shekel": "ILS", - "﷼": "SAR", "sr": "SAR", "sar": "SAR", "riyal": "SAR", "saudi riyal": "SAR", - "د.إ": "AED", "dh": "AED", "aed": "AED", "dirham": "AED", "uae dirham": "AED", - "egp": "EGP", "e£": "EGP", "egyptian pound": "EGP", - "zar": "ZAR", "r": "ZAR", "rand": "ZAR", "south african rand": "ZAR", - "₦": "NGN", "ngn": "NGN", "naira": "NGN", "nigerian naira": "NGN", - } - - // Improved normalization function - normalizeCurrency := func(input string) string { - clean := strings.TrimSpace(strings.ToLower(input)) - clean = strings.Join(strings.Fields(clean), " ") - // Direct map - if mapped, ok := currencyMap[clean]; ok { - return mapped - } - // Fuzzy match: for last word - words := strings.Fields(clean) - for i := 0; i < len(words); i++ { - sub := strings.Join(words[i:], " ") - if mapped, ok := currencyMap[sub]; ok { - return mapped - } - } - // Fuzzy match: try reducing phrase from the end - for i := len(words) - 1; i >= 0; i-- { - sub := strings.Join(words[:i], " ") - if mapped, ok := currencyMap[sub]; ok { - return mapped - } - } - // Handle currency symbols at the end (e.g. "100usd") - if len(clean) > 1 { - if symbol, ok := currencyMap[string(clean[len(clean)-1])]; ok { - return symbol - } - } - // Currency code fallback - if len(clean) == 3 { - upper := strings.ToUpper(clean) - exchangeCacheMutex.RLock() - defer exchangeCacheMutex.RUnlock() - if _, exists := exchangeRates[upper]; exists { - return upper - } - } - return strings.ToUpper(input) - } - - fromCurr := normalizeCurrency(matches[2]) - toCurr := normalizeCurrency(matches[3]) - - // Validate currencies exist in exchange rates - exchangeCacheMutex.RLock() - defer exchangeCacheMutex.RUnlock() - if _, fromExists := exchangeRates[fromCurr]; !fromExists { - return 0, "", "", false - } - if _, toExists := exchangeRates[toCurr]; !toExists { - return 0, "", "", false - } - - return amount, fromCurr, toCurr, true -} - -// ConvertCurrency handles the actual conversion -func ConvertCurrency(amount float64, from, to string) (float64, bool) { - if from == to { - return amount, true - } - - rate, ok := GetExchangeRate(from, to) - if !ok { - // Try to find similar currencies - from = strings.ToUpper(from) - to = strings.ToUpper(to) - - // Check if we have the currency in our list - exchangeCacheMutex.RLock() - defer exchangeCacheMutex.RUnlock() - - _, fromExists := exchangeRates[from] - _, toExists := exchangeRates[to] - - if !fromExists || !toExists { - return 0, false - } - - // Shouldn't happen due to the check above, but just in case - return 0, false - } - - return amount * rate, true -} diff --git a/ia-main.go b/ia-main.go deleted file mode 100644 index 0e90477..0000000 --- a/ia-main.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "fmt" - "time" -) - -type InstantAnswer struct { - Type string // "calc", "unit_convert", "wiki", ... - Title string - Content interface{} -} - -func detectInstantAnswer(query string) *InstantAnswer { - // Try currency conversion first (more specific) - if amount, from, to, ok := ParseCurrencyConversion(query); ok { - if result, ok := ConvertCurrency(amount, from, to); ok { - return &InstantAnswer{ - Type: "currency", - Title: "Currency Conversion", - Content: map[string]interface{}{ - "from": from, - "to": to, - "amount": amount, - "result": result, - "display": fmt.Sprintf("%.2f %s = %.2f %s", amount, from, result, to), - }, - } - } - } - - // Try math expression - if result, ok := parseMathExpression(query); ok { - return &InstantAnswer{ - Type: "calc", - Title: "Calculation Result", - Content: result, - } - } - - // Try weather instant answer - if city, forecast, ok := getWeatherForQuery(query); ok { - return &InstantAnswer{ - Type: "weather", - Title: fmt.Sprintf("Weather in %s", city.Name), - Content: map[string]interface{}{ - "city": city.Name, - "country": city.Country, - "lat": city.Lat, - "lon": city.Lon, - "current": forecast.Current, - "forecast": forecast.Forecast, - "display": fmt.Sprintf("%.1f°C, %s", forecast.Current.Temperature, forecast.Current.Condition), - }, - } - } - - // Try Wikipedia search - if title, text, link, ok := getWikipediaSummary(query); ok { - return &InstantAnswer{ - Type: "wiki", - Title: title, - Content: map[string]string{ - "text": text, - "link": link, - }, - } - } - - return nil -} - -func initExchangeRates() { - // Initial synchronous load - if err := UpdateExchangeRates(); err != nil { - printErr("Initial exchange rate update failed: %v", err) - } else { - PrecacheAllCurrencyPairs() - } - - // Pre-cache common wiki terms in background - go func() { - commonTerms := []string{"United States", "Europe", "Technology", "Science", "Mathematics"} - for _, term := range commonTerms { - getWikipediaSummary(term) - } - }() - - // Periodically update cache - ticker := time.NewTicker(30 * time.Minute) - go func() { - for range ticker.C { - if err := UpdateExchangeRates(); err != nil { - printWarn("Periodic exchange rate update failed: %v", err) - } else { - PrecacheAllCurrencyPairs() - } - } - }() -} diff --git a/ia-weather.go b/ia-weather.go deleted file mode 100644 index 5c9f532..0000000 --- a/ia-weather.go +++ /dev/null @@ -1,367 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "regexp" - "strings" - "unicode" - - "golang.org/x/text/unicode/norm" -) - -type WeatherCity struct { - Name string - Country string - Lat float64 - Lon float64 -} -type WeatherCurrent struct { - Temperature float64 - Wind float64 - Humidity int - Condition string -} -type WeatherDay struct { - Date string - MinTemp float64 - MaxTemp float64 - Condition string -} -type WeatherForecast struct { - Current WeatherCurrent - Forecast []WeatherDay -} - -func getWeatherForQuery(query string) (city WeatherCity, forecast WeatherForecast, ok bool) { - // Expanded multi-language weather keywords (40+ languages/dialects) - weatherWords := []string{ - // English - "weather", "forecast", "temperature", "conditions", "meteorology", "outlook", - // Czech/Slovak - "počasí", "předpověď", "teplota", "vlhkost", "srážky", "vítr", "meteo", - // German - "wetter", "vorhersage", "temperatur", "wettervorhersage", "wetterbericht", - // French - "météo", "prévisions", "température", "conditions météo", "prévision météo", - // Spanish - "tiempo", "clima", "pronóstico", "temperatura", "meteorología", "previsión", - // Italian - "tempo", "meteo", "previsioni", "temperatura", "condizioni atmosferiche", - // Portuguese - "tempo", "clima", "previsão", "temperatura", "meteorologia", - // Polish - "pogoda", "prognoza", "temperatura", "warunki atmosferyczne", - // Russian - "погода", "прогноз", "температура", "метео", "метеопрогноз", - // Ukrainian - "погода", "прогноз", "температура", "метео", - // Dutch - "weer", "voorspelling", "temperatuur", "weersverwachting", - // Scandinavian - "väder", "prognos", "temperatur", // Swedish - "vær", "prognose", "temperatur", // Norwegian/Danish - "veður", "spá", "hitastig", // Icelandic - // East Asian - "天気", "予報", "気温", // Japanese (tenki, yohō, kion) - "날씨", "예보", "기온", // Korean (nalssi, yebo, gion) - "天气", "预报", "气温", // Chinese (tiānqì, yùbào, qìwēn) - // South Asian - "मौसम", "पूर्वानुमान", "तापमान", // Hindi (mausam, purvanumaan, taapmaan) - "আবহাওয়া", "পূর্বাভাস", "তাপমাত্রা", // Bengali (ābhawāẏā, pūrbābhāsa, tāpamātrā) - // Middle Eastern - "طقس", "توقعات", "درجة الحرارة", // Arabic (ṭaqs, tawaqquʿāt, darajat al-ḥarāra) - "آب و ہوا", "پیش گوئی", "درجہ حرارت", // Urdu (āb-o-hawā, peshgoī, daraja ḥarārat) - // Turkish - "hava", "tahmin", "sıcaklık", "hava durumu", - // Greek - "καιρός", "πρόβλεψη", "θερμοκρασία", - // Hebrew - "מזג אוויר", "תחזית", "טמפרטורה", - // Other European - "időkép", "előrejelzés", "hőmérséklet", // Hungarian - "vreme", "prognoză", "temperatură", // Romanian - "vrijeme", "prognoza", "temperatura", // Croatian/Serbian - // Global/Internet slang - "temp", "wx", "meteo", "wea", "forec", - } - - // Enhanced multi-language prepositions - prepositions := []string{ - // English - "in", "at", "for", "around", "near", - // Czech/Slovak - "v", "ve", "na", "do", "u", "při", "blízko", "okolí", - // German - "in", "bei", "an", "für", "um", "nahe", - // Romance - "en", "a", "au", "aux", "dans", // French - "en", "a", "de", // Spanish - "a", "in", "da", // Italian - "em", "no", "na", // Portuguese - // Slavic - "w", "we", "na", "dla", "pod", // Polish - "в", "на", "у", "к", "под", // Russian/Ukrainian - // Nordic - "i", "på", "hos", // Swedish/Danish/Norwegian - // Others - "في", "عند", "قرب", // Arabic (fī, ʿind, qurb) - "में", "पर", "के पास", // Hindi (mẽ, par, ke pās) - "で", "に", "の近く", // Japanese (de, ni, no chikaku) - "에서", "에", "근처", // Korean (eseo, e, geuncheo) - "在", "于", "附近", // Chinese (zài, yú, fùjìn) - } - - // Always normalize query (lowercase + remove diacritics) - normalized := removeDiacritics(strings.ToLower(query)) - hasWeather := false - for _, word := range weatherWords { - if strings.Contains(normalized, removeDiacritics(word)) { - hasWeather = true - break - } - } - if !hasWeather { - return city, forecast, false - } - - // Improved location extraction with diacritic handling - loc := extractWeatherLocation(normalized, weatherWords, prepositions) - if loc == "" { - return city, forecast, false - } - - // Geocode and get weather - return geocodeAndGetWeather(loc) -} - -func extractWeatherLocation(query string, weatherWords, prepositions []string) string { - // Create normalized versions for matching - normWeatherWords := make([]string, len(weatherWords)) - for i, w := range weatherWords { - normWeatherWords[i] = removeDiacritics(w) - } - - normPrepositions := make([]string, len(prepositions)) - for i, p := range prepositions { - normPrepositions[i] = removeDiacritics(p) - } - - // Pattern 1: [weather_word] [preposition]? [location] - pattern1 := `(?:` + strings.Join(normWeatherWords, "|") + `)\s*(?:` + strings.Join(normPrepositions, "|") + `)?\s*(.+)` - re1 := regexp.MustCompile(pattern1) - if matches := re1.FindStringSubmatch(query); len(matches) > 1 { - loc := cleanLocation(matches[1], normPrepositions) - if loc != "" { - return loc - } - } - - // Pattern 2: [location] [weather_word] - pattern2 := `(.+?)\s+(?:` + strings.Join(normWeatherWords, "|") + `)` - re2 := regexp.MustCompile(pattern2) - if matches := re2.FindStringSubmatch(query); len(matches) > 1 { - loc := cleanLocation(matches[1], normPrepositions) - if loc != "" { - return loc - } - } - - // Pattern 3: Question format - questionPattern := `(?:how is|what is|what's|jak[ée]\s+je|wie ist|quel est|qu[eé]\s+tal|com'[èe])\s+(?:the )?(?:` + - strings.Join(normWeatherWords, "|") + `)\s*(?:` + strings.Join(normPrepositions, "|") + `)?\s*(.+)` - re3 := regexp.MustCompile(questionPattern) - if matches := re3.FindStringSubmatch(query); len(matches) > 1 { - loc := cleanLocation(matches[1], normPrepositions) - if loc != "" { - return loc - } - } - - // Fallback with smarter exclusion - return extractByExclusion(query, normWeatherWords, normPrepositions) -} - -func cleanLocation(loc string, prepositions []string) string { - // Create preposition set - prepSet := make(map[string]bool) - for _, p := range prepositions { - prepSet[p] = true - } - - words := strings.Fields(loc) - - // Remove leading prepositions - for len(words) > 0 && prepSet[words[0]] { - words = words[1:] - } - - // Remove trailing prepositions - for len(words) > 0 && prepSet[words[len(words)-1]] { - words = words[:len(words)-1] - } - - // Rejoin and clean - cleaned := strings.Join(words, " ") - return strings.Trim(cleaned, ",.?!:;()[]{}'\"") -} - -// Remove diacritics implementation -func removeDiacritics(s string) string { - var result []rune - for _, r := range norm.NFD.String(s) { - if unicode.Is(unicode.Mn, r) { // Mn: nonspacing marks - continue - } - result = append(result, r) - } - return string(result) -} - -// Extract location by removing weather-related words -func extractByExclusion(query string, weatherWords, prepositions []string) string { - // Create removal set - removeSet := make(map[string]bool) - for _, w := range weatherWords { - removeSet[w] = true - } - for _, p := range prepositions { - removeSet[p] = true - } - - // Process query words - words := strings.Fields(query) - var locWords []string - for _, word := range words { - if !removeSet[word] { - locWords = append(locWords, word) - } - } - - loc := strings.Join(locWords, " ") - return cleanLocation(loc, prepositions) -} - -// // Improved location cleaning -// func cleanLocation(loc string) string { -// loc = strings.Trim(loc, ",.?!:;()[]{}'\"") - -// // Remove trailing verbs -// verbs := []string{"is", "at", "for", "in", "v", "ve", "na", "do", "w", "en", "a"} -// for _, v := range verbs { -// loc = strings.TrimSuffix(loc, " "+v) -// } - -// return loc -// } - -// // Remove diacritics implementation -// func removeDiacritics(s string) string { -// var result []rune -// for _, r := range norm.NFD.String(s) { -// if unicode.Is(unicode.Mn, r) { // Mn: nonspacing marks -// continue -// } -// result = append(result, r) -// } -// return string(result) -// } - -func geocodeAndGetWeather(loc string) (WeatherCity, WeatherForecast, bool) { - var city WeatherCity - var forecast WeatherForecast - // 1. Geocode - geoURL := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1", urlQueryEscape(loc)) - resp, err := http.Get(geoURL) - if err != nil { - return city, forecast, false - } - defer resp.Body.Close() - var geo struct { - Results []struct { - Name string `json:"name"` - Country string `json:"country"` - Lat float64 `json:"latitude"` - Lon float64 `json:"longitude"` - } `json:"results"` - } - if err := json.NewDecoder(resp.Body).Decode(&geo); err != nil || len(geo.Results) == 0 { - return city, forecast, false - } - g := geo.Results[0] - city = WeatherCity{ - Name: g.Name, - Country: g.Country, - Lat: g.Lat, - Lon: g.Lon, - } - // 2. Weather (current + forecast) - weatherURL := fmt.Sprintf("https://api.open-meteo.com/v1/forecast?latitude=%f&longitude=%f¤t=temperature_2m,weather_code,wind_speed_10m,relative_humidity_2m&daily=temperature_2m_min,temperature_2m_max,weather_code&forecast_days=3&timezone=auto", g.Lat, g.Lon) - resp2, err := http.Get(weatherURL) - if err != nil { - return city, forecast, false - } - defer resp2.Body.Close() - var data struct { - Current struct { - Temp float64 `json:"temperature_2m"` - Wind float64 `json:"wind_speed_10m"` - Hum int `json:"relative_humidity_2m"` - Code int `json:"weather_code"` - } `json:"current"` - Daily struct { - Dates []string `json:"time"` - MinTemp []float64 `json:"temperature_2m_min"` - MaxTemp []float64 `json:"temperature_2m_max"` - Weather []int `json:"weather_code"` - } `json:"daily"` - } - body, _ := io.ReadAll(resp2.Body) - if err := json.Unmarshal(body, &data); err != nil { - return city, forecast, false - } - forecast.Current = WeatherCurrent{ - Temperature: data.Current.Temp, - Wind: data.Current.Wind, - Humidity: data.Current.Hum, - Condition: weatherDescription(data.Current.Code), - } - for i := range data.Daily.Dates { - forecast.Forecast = append(forecast.Forecast, WeatherDay{ - Date: data.Daily.Dates[i], - MinTemp: data.Daily.MinTemp[i], - MaxTemp: data.Daily.MaxTemp[i], - Condition: weatherDescription(data.Daily.Weather[i]), - }) - } - return city, forecast, true -} - -func weatherDescription(code int) string { - // Minimal mapping, can be expanded - switch code { - case 0: - return "Clear" - case 1, 2, 3: - return "Partly cloudy" - case 45, 48: - return "Fog" - case 51, 53, 55, 56, 57: - return "Drizzle" - case 61, 63, 65, 66, 67, 80, 81, 82: - return "Rain" - case 71, 73, 75, 77, 85, 86: - return "Snow" - case 95, 96, 99: - return "Thunderstorm" - default: - return "Unknown" - } -} - -// Helper for safe query escaping -func urlQueryEscape(s string) string { - return strings.ReplaceAll(strings.ReplaceAll(s, " ", "+"), "%", "") -} diff --git a/ia-wiki.go b/ia-wiki.go deleted file mode 100644 index 3e9fe3c..0000000 --- a/ia-wiki.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "strings" -) - -// Wikipedia API response structure -type WikipediaResponse struct { - Query struct { - Pages map[string]struct { - PageID int `json:"pageid"` - Title string `json:"title"` - Extract string `json:"extract"` - } `json:"pages"` - } `json:"query"` -} - -// Get Wikipedia summary -func getWikipediaSummary(query string) (title, text, link string, ok bool) { - // Clean and prepare query - query = strings.TrimSpace(query) - if query == "" { - return "", "", "", false - } - - // API request - apiURL := fmt.Sprintf( - "https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts&exintro&explaintext&redirects=1&titles=%s", - url.QueryEscape(query), - ) - - resp, err := http.Get(apiURL) - if err != nil { - return "", "", "", false - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", "", "", false - } - - // Parse JSON response - var result WikipediaResponse - if err := json.Unmarshal(body, &result); err != nil { - return "", "", "", false - } - - // Extract first valid page - for _, page := range result.Query.Pages { - if page.PageID == 0 || page.Extract == "" { - continue - } - - // Format text - text = page.Extract - if len(text) > 500 { - text = text[:500] + "..." - } - - // Create link - titleForURL := strings.ReplaceAll(page.Title, " ", "_") - link = fmt.Sprintf("https://en.wikipedia.org/wiki/%s", url.PathEscape(titleForURL)) - - return page.Title, text, link, true - } - - return "", "", "", false -} 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 87dceb5..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,8 +86,8 @@ func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string select { case results := <-cacheChan: if results == nil { - if config.MetaSearchEnabled { - combinedResults = fetchImageResults(query, safe, lang, page, synchronous, true) + if config.CrawlerEnabled { + combinedResults = fetchImageResults(query, safe, lang, page, synchronous) if len(combinedResults) > 0 { combinedResults = filterValidImages(combinedResults) resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) @@ -107,13 +96,13 @@ 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 { - combinedResults = fetchImageResults(query, safe, lang, page, synchronous, true) + if config.CrawlerEnabled { + combinedResults = fetchImageResults(query, safe, lang, page, synchronous) if len(combinedResults) > 0 { combinedResults = filterValidImages(combinedResults) resultsCache.Set(cacheKey, convertToSearchResults(combinedResults)) @@ -126,21 +115,15 @@ func getImageResultsFromCacheOrFetch(cacheKey CacheKey, query, safe, lang string return combinedResults } -func fetchImageResults(query, safe, lang string, page int, synchronous bool, thumbsNeeded bool) []ImageSearchResult { +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 } - // This will not happen as during config load there is check to have at least something in search engine list - // if len(imageSearchEngines) == 0 { - // printWarn("No image search engines configured in imageSearchEngines") - // return nil - // } - engineCount := len(imageSearchEngines) // Determine the engine to use based on the page number @@ -180,7 +163,7 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool, thu 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) @@ -237,25 +220,23 @@ func fetchImageResults(query, safe, lang string, page int, synchronous bool, thu imageURLMapMu.Unlock() if config.DriveCacheEnabled { - if thumbsNeeded { - go func(imgResult ImageSearchResult) { - _, success, err := cacheImage(imgResult.Thumb, imgResult.ID, "thumb") - if err != nil || !success { - printWarn("Failed to cache thumbnail image %s: %v", imgResult.Thumb, err) - removeImageResultFromCache(query, page, safe == "active", lang, imgResult.ID) - } - }(imageResult) - imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb.webp", hash) - } else { - imageResult.ProxyThumb = "" // fallback ? - } + // Cache the thumbnail image asynchronously + go func(imgResult ImageSearchResult) { + _, 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) + } + }(imageResult) + + // Set ProxyThumb to the proxy URL (initially placeholder) + imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb.webp", hash) + + // Set ProxyFull to the proxy URL imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash) } else { - if thumbsNeeded { - imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb", hash) - } else { - imageResult.ProxyThumb = "" - } + // Hard cache disabled, proxy both thumb and full images + imageResult.ProxyThumb = fmt.Sprintf("/image/%s_thumb", hash) imageResult.ProxyFull = fmt.Sprintf("/image/%s_full", hash) } diff --git a/indexer.go b/indexer.go index 0f5b5dd..c8cf6fe 100644 --- a/indexer.go +++ b/indexer.go @@ -1,6 +1,3 @@ -//go:build experimental -// +build experimental - package main import ( diff --git a/init-extra.go b/init-extra.go deleted file mode 100644 index c455d41..0000000 --- a/init-extra.go +++ /dev/null @@ -1,148 +0,0 @@ -//go:build experimental -// +build experimental - -package main - -import ( - "flag" - "os" - "path/filepath" -) - -var config Config - -func main() { - // Command-line flags - 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 { - // Load from config file if it exists - config = loadConfig() - } else { - // Use defaults if config file does not exist - config = defaultConfig - saveConfig(config) // Save the defaults to config.ini - printInfo("Configuration saved to %s", configFilePath) - } - } else { - // Initialize configuration interactively or from config file - err := initConfig() - if err != nil { - printErr("Error during initialization: %v", err) - return - } - } - - // Override with command-line arguments if provided - if *portFlag != 0 { - config.Port = *portFlag - } - if *domainFlag != "" { - config.Domain = *domainFlag - } - - loadNodeConfig() - - if config.CrawlerProxyEnabled || config.MetaProxyEnabled { - InitProxies() - } - - // Initiate Browser Agent updater - if config.MetaSearchEnabled || config.IndexerEnabled { - go periodicAgentUpdate() - } - - // Load List of Meta Search Engines - if config.MetaSearchEnabled { - initTextEngines() - initImageEngines() - initFileEngines() - initPipedInstances() - initMusicEngines() - initExchangeRates() - } - - InitializeLanguage("en") // Initialize language before generating OpenSearch - generateOpenSearchXML(config) - - // Start the node client only if NodesEnabled is true - if config.NodesEnabled { - go startUnixSocketServer(config.NodeID) - printInfo("Node client started.") - } else { - printInfo("Node client is disabled.") - } - - // Check if the cache directory exists when caching is enabled - if config.DriveCacheEnabled { - cacheDir := config.DriveCache.Path - imagesDir := filepath.Join(cacheDir, "images") - - // Check if the directory already exists - if _, err := os.Stat(imagesDir); os.IsNotExist(err) { - // Try to create the directory since it doesn't exist - if err := os.MkdirAll(imagesDir, os.ModePerm); err != nil { - printErr("Error: Failed to create cache or images directory '%s': %v", imagesDir, err) - os.Exit(1) // Exit with a non-zero status to indicate an error - } - // Print a warning if the directory had to be created - printWarn("Warning: Created missing directory '%s'.", imagesDir) - } - } - - // Start periodic cleanup of expired cache files - if config.DriveCacheEnabled { - go cleanExpiredCachedImages() - printInfo("Drive cache started.") - } else { - printInfo("Drive cache is disabled.") - } - - // Start periodic cleanup of expired cache files - if config.RamCacheEnabled { - resultsCache = NewResultsCache() - geocodeCache = NewGeocodeCache() - printInfo("RAM cache started.") - } else { - printInfo("RAM cache is disabled.") - } - - // Init indexer - if config.IndexerEnabled { - if err := downloadAndSetupDomainsCSV(); err != nil { - printErr("Failed to set up domains.csv: %v", err) - return - } - - err := InitIndex() - if err != nil { - printErr("Failed to initialize index: %v", err) - } - - webCrawlerInit() - - printInfo("Indexer is enabled.") - } else { - printInfo("Indexer is disabled.") - } - - // if len(config.MetaSearch.Text) == 0 { - // log.Fatal("No text search engines are enabled in config (MetaSearch.Text)") - // } - // fmt.Printf("Loaded config.MetaSearch.Text: %#v\n", config.MetaSearch.Text) - - runServer() -} diff --git a/init.go b/init.go index 30f5345..bf0d220 100644 --- a/init.go +++ b/init.go @@ -1,6 +1,3 @@ -//go:build !experimental -// +build !experimental - package main import ( @@ -12,21 +9,14 @@ import ( var config Config func main() { - // Command-line flags 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 { @@ -55,28 +45,37 @@ func main() { config.Domain = *domainFlag } - if config.MetaProxyEnabled { - InitProxies() + loadNodeConfig() + + if config.AuthCode == "" { + config.AuthCode = generateStrongRandomString(64) + printInfo("Generated connection code: %s\n", config.AuthCode) + saveConfig(config) } + // Generate Host ID + hostID, nodeErr := generateHostID() + if nodeErr != nil { + printErr("Failed to generate host ID: %v", nodeErr) + } + config.PeerID = hostID + // 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() - initExchangeRates() - } - InitializeLanguage("en") // Initialize language before generating OpenSearch generateOpenSearchXML(config) + // Start the node client only if NodesEnabled is true + if config.NodesEnabled { + go startNodeClient() + printInfo("Node client started.") + } else { + printInfo("Node client is disabled.") + } + // Check if the cache directory exists when caching is enabled if config.DriveCacheEnabled { cacheDir := config.DriveCache.Path @@ -111,10 +110,29 @@ func main() { printInfo("RAM cache is disabled.") } - // if len(config.MetaSearch.Text) == 0 { - // log.Fatal("No text search engines are enabled in config (MetaSearch.Text)") - // } - // fmt.Printf("Loaded config.MetaSearch.Text: %#v\n", config.MetaSearch.Text) + // Init indexer + if config.IndexerEnabled { + if err := downloadAndSetupDomainsCSV(); err != nil { + printErr("Failed to set up domains.csv: %v", err) + return + } + + err := InitIndex() + if err != nil { + printErr("Failed to initialize index: %v", err) + } + + 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.") + } runServer() } diff --git a/lang/af/LC_MESSAGES/default.po b/lang/af/LC_MESSAGES/default.po index 183df07..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" @@ -184,6 +184,8 @@ msgstr "Strate" msgid "satellite" msgstr "Satelliet" +msgid "esri_satellite" +msgstr "Esri Satelliet" msgid "topographic" msgstr "Topografiese" @@ -196,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 90b5c5b..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 "عدد المزودين" @@ -184,6 +184,8 @@ msgstr "شوارع" msgid "satellite" msgstr "قمر صناعي" +msgid "esri_satellite" +msgstr "قمر صناعي ESRI" msgid "topographic" msgstr "طوبوغرافي" @@ -196,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 29025ae..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 "Па колькасці сейдэраў" @@ -184,6 +184,8 @@ msgstr "Вуліцы" msgid "satellite" msgstr "Спадарожнік" +msgid "esri_satellite" +msgstr "Спадарожнік ESRI" msgid "topographic" msgstr "Тапаграфічная" @@ -196,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 92648fd..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 "Сийдъри (качване)" @@ -184,6 +184,8 @@ msgstr "Улици" msgid "satellite" msgstr "Сателит" +msgid "esri_satellite" +msgstr "ESRI Сателит" msgid "topographic" msgstr "Топографска" @@ -196,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 cb886e8..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" @@ -184,6 +184,8 @@ msgstr "Carrers" msgid "satellite" msgstr "Satèl·lit" +msgid "esri_satellite" +msgstr "Satèl·lit ESRI" msgid "topographic" msgstr "Topogràfic" @@ -196,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 fdd1806..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ů" @@ -184,6 +184,8 @@ msgstr "Ulice" msgid "satellite" msgstr "Satelitní" +msgid "esri_satellite" +msgstr "Esri Satelitní" msgid "topographic" msgstr "Topografická" @@ -195,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 bc163b2..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" @@ -184,6 +184,8 @@ msgstr "Gader" msgid "satellite" msgstr "Satellit" +msgid "esri_satellite" +msgstr "ESRI Satellit" msgid "topographic" msgstr "Topografisk" @@ -196,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 8de8282..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" @@ -184,6 +184,8 @@ msgstr "Straßen" msgid "satellite" msgstr "Satellit" +msgid "esri_satellite" +msgstr "ESRI-Satellit" msgid "topographic" msgstr "Topographisch" @@ -196,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 defbc98..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" @@ -184,6 +184,8 @@ msgstr "Δρόμοι" msgid "satellite" msgstr "Δορυφόρος" +msgid "esri_satellite" +msgstr "ESRI Δορυφόρος" msgid "topographic" msgstr "Τοπογραφικός" @@ -196,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 e00fd81..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" @@ -190,6 +184,8 @@ msgstr "Streets" msgid "satellite" msgstr "Satellite" +msgid "esri_satellite" +msgstr "Esri Satellite" msgid "topographic" msgstr "Topographic" @@ -202,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 492ce04..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" @@ -184,6 +184,8 @@ msgstr "Stratoj" msgid "satellite" msgstr "Satelito" +msgid "esri_satellite" +msgstr "ESRI Satelito" msgid "topographic" msgstr "Topografia" @@ -196,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 6a7aaef..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" @@ -184,6 +184,8 @@ msgstr "Calles" msgid "satellite" msgstr "Satélite" +msgid "esri_satellite" +msgstr "Satélite ESRI" msgid "topographic" msgstr "Topográfico" @@ -196,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 eaf212c..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" @@ -184,6 +184,8 @@ msgstr "Tänavad" msgid "satellite" msgstr "Satelliit" +msgid "esri_satellite" +msgstr "ESRI Satelliit" msgid "topographic" msgstr "Topograafiline" @@ -196,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 f3f2f7f..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 "مرتب‌سازی بر اساس سیدرها" @@ -184,6 +184,8 @@ msgstr "خیابان‌ها" msgid "satellite" msgstr "ماهواره" +msgid "esri_satellite" +msgstr "ماهواره ESRI" msgid "topographic" msgstr "توپوگرافی" @@ -196,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 1539958..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" @@ -184,6 +184,8 @@ msgstr "Kadut" msgid "satellite" msgstr "Satelliitti" +msgid "esri_satellite" +msgstr "ESRI Satelliitti" msgid "topographic" msgstr "Topografinen" @@ -196,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 437639d..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" @@ -184,6 +184,8 @@ msgstr "Rues" msgid "satellite" msgstr "Satellite" +msgid "esri_satellite" +msgstr "Satellite ESRI" msgid "topographic" msgstr "Topographique" @@ -196,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 e769c0a..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 "सीडर्स के अनुसार छांटें" @@ -184,6 +184,8 @@ msgstr "सड़कें" msgid "satellite" msgstr "सैटेलाइट" +msgid "esri_satellite" +msgstr "ESRI सैटेलाइट" msgid "topographic" msgstr "टोपोग्राफिक" @@ -196,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 b01dea6..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)" @@ -184,6 +184,8 @@ msgstr "Ulice" msgid "satellite" msgstr "Satelit" +msgid "esri_satellite" +msgstr "ESRI Satelit" msgid "topographic" msgstr "Topografski" @@ -196,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 7376e96..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" @@ -184,6 +184,8 @@ msgstr "Utcák" msgid "satellite" msgstr "Műhold" +msgid "esri_satellite" +msgstr "ESRI Műhold" msgid "topographic" msgstr "Topográfiai" @@ -196,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 6e0ab2d..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 "Ներբեռնում (արտահանող)" @@ -184,6 +184,8 @@ msgstr "Փողոցներ" msgid "satellite" msgstr "Արհեստական արբանյակ" +msgid "esri_satellite" +msgstr "ESRI Արհեստական արբանյակ" msgid "topographic" msgstr "Տոպոգրաֆիկ" @@ -196,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 8717ed9..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" @@ -184,6 +184,8 @@ msgstr "Jalan" msgid "satellite" msgstr "Satelit" +msgid "esri_satellite" +msgstr "Satelit ESRI" msgid "topographic" msgstr "Topografi" @@ -196,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 b8aeccb..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" @@ -184,6 +184,8 @@ msgstr "Strade" msgid "satellite" msgstr "Satellitare" +msgid "esri_satellite" +msgstr "Satellitare ESRI" msgid "topographic" msgstr "Topografico" @@ -196,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 94e40dd..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 "מיון לפי משתפים" @@ -184,6 +184,8 @@ msgstr "רחובות" msgid "satellite" msgstr "לוויין" +msgid "esri_satellite" +msgstr "לוויין ESRI" msgid "topographic" msgstr "טופוגרפי" @@ -196,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 e7408ab..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 "シーダーで並べ替え" @@ -184,6 +184,8 @@ msgstr "ストリート" msgid "satellite" msgstr "衛星" +msgid "esri_satellite" +msgstr "ESRI 衛星" msgid "topographic" msgstr "地形図" @@ -196,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 92569ec..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 "시더 기준 정렬" @@ -184,6 +184,8 @@ msgstr "거리" msgid "satellite" msgstr "위성" +msgid "esri_satellite" +msgstr "ESRI 위성" msgid "topographic" msgstr "지형도" @@ -196,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 ec2181d..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" @@ -184,6 +184,8 @@ msgstr "Gatvės" msgid "satellite" msgstr "Palydovas" +msgid "esri_satellite" +msgstr "ESRI palydovas" msgid "topographic" msgstr "Topografinis" @@ -196,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 b0a57da..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" @@ -184,6 +184,8 @@ msgstr "Ielas" msgid "satellite" msgstr "Satelīts" +msgid "esri_satellite" +msgstr "ESRI satelīts" msgid "topographic" msgstr "Topogrāfiskais" @@ -196,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 2a0c44b..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" @@ -184,6 +184,8 @@ msgstr "Straten" msgid "satellite" msgstr "Satelliet" +msgid "esri_satellite" +msgstr "ESRI Satelliet" msgid "topographic" msgstr "Topografisch" @@ -196,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 77201db..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" @@ -184,6 +184,8 @@ msgstr "Gater" msgid "satellite" msgstr "Satellitt" +msgid "esri_satellite" +msgstr "ESRI Satellitt" msgid "topographic" msgstr "Topografisk" @@ -196,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 31c5e6f..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" @@ -184,6 +184,8 @@ msgstr "Ulice" msgid "satellite" msgstr "Satelita" +msgid "esri_satellite" +msgstr "Esri Satelita" msgid "topographic" msgstr "Topograficzna" @@ -195,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 a6f1874..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" @@ -184,6 +184,8 @@ msgstr "Ruas" msgid "satellite" msgstr "Satélite" +msgid "esri_satellite" +msgstr "Satélite ESRI" msgid "topographic" msgstr "Topográfico" @@ -196,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 20a7134..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" @@ -184,6 +184,8 @@ msgstr "Străzi" msgid "satellite" msgstr "Satelit" +msgid "esri_satellite" +msgstr "Satelit ESRI" msgid "topographic" msgstr "Topografic" @@ -196,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 8007059..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 "Сортировать по сидерам" @@ -184,6 +184,8 @@ msgstr "Улицы" msgid "satellite" msgstr "Спутник" +msgid "esri_satellite" +msgstr "Спутник ESRI" msgid "topographic" msgstr "Топографическая" @@ -196,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 e2ba122..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" @@ -184,6 +184,8 @@ msgstr "Ulice" msgid "satellite" msgstr "Satelit" +msgid "esri_satellite" +msgstr "ESRI Satelit" msgid "topographic" msgstr "Topografické" @@ -196,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 26d1bc7..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" @@ -184,6 +184,8 @@ msgstr "Ulice" msgid "satellite" msgstr "Satelit" +msgid "esri_satellite" +msgstr "ESRI satelit" msgid "topographic" msgstr "Topografsko" @@ -196,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 b05fdc0..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 "Сортирај по сеедерима" @@ -184,6 +184,8 @@ msgstr "Улице" msgid "satellite" msgstr "Сателит" +msgid "esri_satellite" +msgstr "ESRI сателит" msgid "topographic" msgstr "Топографска" @@ -196,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 99d06b1..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" @@ -184,6 +184,8 @@ msgstr "Gator" msgid "satellite" msgstr "Satellit" +msgid "esri_satellite" +msgstr "ESRI Satellit" msgid "topographic" msgstr "Topografisk" @@ -196,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 326efd1..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" @@ -184,6 +184,8 @@ msgstr "Mitaa" msgid "satellite" msgstr "Setilaiti" +msgid "esri_satellite" +msgstr "Setilaiti ya ESRI" msgid "topographic" msgstr "Topografia" @@ -196,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 cda462b..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 "จัดเรียงตามซีดเดอร์" @@ -184,6 +184,8 @@ msgstr "ถนน" msgid "satellite" msgstr "ดาวเทียม" +msgid "esri_satellite" +msgstr "ดาวเทียม ESRI" msgid "topographic" msgstr "ภูมิประเทศ" @@ -196,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 41b467a..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" @@ -184,6 +184,8 @@ msgstr "Mga Kalye" msgid "satellite" msgstr "Satelite" +msgid "esri_satellite" +msgstr "ESRI Satelite" msgid "topographic" msgstr "Topograpiko" @@ -196,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 edfdeb8..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" @@ -184,6 +184,8 @@ msgstr "Sokaklar" msgid "satellite" msgstr "Uydu" +msgid "esri_satellite" +msgstr "ESRI Uydu" msgid "topographic" msgstr "Topografik" @@ -196,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 d5b8b38..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 "Сортувати за сідерами" @@ -184,6 +184,8 @@ msgstr "Вулиці" msgid "satellite" msgstr "Супутник" +msgid "esri_satellite" +msgstr "Супутник ESRI" msgid "topographic" msgstr "Топографічна" @@ -196,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 3006edd..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" @@ -184,6 +184,8 @@ msgstr "Đường phố" msgid "satellite" msgstr "Vệ tinh" +msgid "esri_satellite" +msgstr "Vệ tinh ESRI" msgid "topographic" msgstr "Địa hình" @@ -196,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 f013786..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 "排序:上传者" @@ -184,6 +184,8 @@ msgstr "街道" msgid "satellite" msgstr "卫星" +msgid "esri_satellite" +msgstr "ESRI 卫星" msgid "topographic" msgstr "地形图" @@ -196,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 365d9fc..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 "排序(種子數量)" @@ -184,6 +184,8 @@ msgstr "街道" msgid "satellite" msgstr "衛星" +msgid "esri_satellite" +msgstr "ESRI 衛星" msgid "topographic" msgstr "地形" @@ -196,9 +198,3 @@ msgstr "您在 " msgid "meters_from_point" msgstr "公尺範圍內" - -msgid "seconds" -msgstr "秒" - -msgid "milliseconds" -msgstr "毫秒" diff --git a/main.go b/main.go index 7b72235..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": @@ -175,7 +173,7 @@ func handleSearch(w http.ResponseWriter, r *http.Request) { case "text": fallthrough default: - HandleTextSearchWithInstantAnswer(w, settings, query, page) + HandleTextSearch(w, settings, query, page) } } @@ -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,11 @@ 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 { + http.HandleFunc("/node", handleNodeRequest) } printMessage("Server is listening on http://localhost:%d", config.Port) @@ -250,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) { @@ -278,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 2aa9f46..0000000 --- a/music-soundcloud.go +++ /dev/null @@ -1,211 +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, - ) - - // Convert ms to hh:mm:ss - totalSeconds := track.Duration / 1000 - hours := totalSeconds / 3600 - minutes := (totalSeconds % 3600) / 60 - seconds := totalSeconds % 60 - - var durationStr string - if hours > 0 { - durationStr = fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds) - } else { - durationStr = fmt.Sprintf("%d:%02d", minutes, seconds) - } - - results = append(results, MusicResult{ - Title: track.Title, - Artist: track.User.Username, - URL: trackURL, - Thumbnail: thumbnail, - //AudioURL: track.Streams.HTTPMP3128URL, - Source: "SoundCloud", - Duration: durationStr, - }) - } - 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 d262428..0000000 --- a/music-youtube.go +++ /dev/null @@ -1,113 +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 - - // We will try to use preferred instance - mu.Lock() - instance := preferredInstance - mu.Unlock() - - if instance != "" && !disabledInstances[instance] { - 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 { - defer resp.Body.Close() - var apiResp MusicAPIResponse - if err := json.NewDecoder(resp.Body).Decode(&apiResp); err == nil { - return convertPipedToMusicResults(instance, apiResp), nil - } - } - - printWarn("Preferred instance %s failed for music, falling back", instance) - disableInstance(instance) - } - - // 2. Fallback using others - mu.Lock() - defer mu.Unlock() - for _, inst := range pipedInstances { - if disabledInstances[inst] { - continue - } - - url := fmt.Sprintf( - "https://%s/search?q=%s&filter=music_songs&page=%d", - inst, - url.QueryEscape(query), - page, - ) - - resp, err := http.Get(url) - if err != nil || resp.StatusCode != http.StatusOK { - printInfo("Disabling instance %s due to error: %v", inst, err) - disabledInstances[inst] = true - lastError = fmt.Errorf("request to %s failed: %w", inst, 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", inst, err) - continue - } - - preferredInstance = inst - return convertPipedToMusicResults(inst, 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 76518e2..0000000 --- a/music.go +++ /dev/null @@ -1,177 +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 - "NoResults": len(results) == 0, - "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-handle-search.go b/node-handle-search.go index bdb7c8f..7323710 100755 --- a/node-handle-search.go +++ b/node-handle-search.go @@ -1,203 +1,218 @@ -//go:build experimental -// +build experimental - package main import ( "encoding/json" + "log" ) -type searchParams struct { - Query string `json:"query"` - Safe string `json:"safe"` - Lang string `json:"lang"` - Page int `json:"page"` - ResponseAddr string `json:"responseAddr"` -} - -func extractTargetFromAddress(addr string) string { - if len(addr) > 5 && addr[len(addr)-5:] == ".sock" { - return addr[:len(addr)-5] - } - return addr -} - -// Utility to respond to any search -func respondToSearch(req searchParams, msgType uint8, results any) { - if req.ResponseAddr == "" { - printErr("ResponseAddr is empty") - return - } - - respBytes, err := json.Marshal(results) - if err != nil { - printWarn("Failed to marshal results for msg type %d: %v", msgType, err) - return - } - - resp := Message{ - ID: generateMessageID(), - Type: msgType, - Content: respBytes, - Target: req.ResponseAddr, - } - - err = sendMessage(resp) - if err != nil { - printWarn("Failed to send search results to %s: %v", req.ResponseAddr, err) - } -} - -func sendBinaryResponse(req searchParams, msgType uint8, payload []byte, msgID uint32) { - if req.ResponseAddr == "" { - printErr("ResponseAddr is empty") - return - } - - resp := Message{ - ID: msgID, - Type: msgType, - Content: payload, - Target: req.ResponseAddr, - } - - if err := sendMessage(resp); err != nil { - printWarn("Failed to send binary search results: %v", err) - } -} - func handleSearchTextMessage(msg Message) { - var req searchParams - if err := json.Unmarshal([]byte(msg.Content), &req); err != nil { - printWarn("Invalid JSON: %v", err) - return + var searchParams struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` } - printDebug("Received search-text from %s", req.ResponseAddr) - - results := fetchTextResults(req.Query, req.Safe, req.Lang, req.Page) - data, err := encodeTextResults(results) + err := json.Unmarshal([]byte(msg.Content), &searchParams) if err != nil { - printWarn("Failed to encode text results: %v", err) + printWarn("Error parsing search parameters: %v", err) return } - sendBinaryResponse(req, MsgTypeSearchTextResponse, data, msg.ID) -} + printDebug("Received search-text request. ResponseAddr: %s", searchParams.ResponseAddr) -func handleSearchVideoMessage(msg Message) { - var req searchParams - if err := json.Unmarshal([]byte(msg.Content), &req); err != nil { - printWarn("Invalid JSON: %v", err) - return - } - printDebug("Received search-video from %s", req.ResponseAddr) - - results := fetchVideoResults(req.Query, req.Safe, req.Lang, req.Page) - data, err := encodeVideoResults(results) + results := fetchTextResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) + resultsJSON, err := json.Marshal(results) if err != nil { - printWarn("Failed to encode video results: %v", err) + printWarn("Error marshalling search results: %v", err) return } - sendBinaryResponse(req, MsgTypeSearchVideoResponse, data, msg.ID) -} + responseMsg := Message{ + ID: hostID, + Type: "text-results", + Content: string(resultsJSON), + } -func handleSearchMusicMessage(msg Message) { - var req searchParams - if err := json.Unmarshal([]byte(msg.Content), &req); err != nil { - printWarn("Invalid JSON: %v", err) + // Log the address to be used for sending the response + printDebug("Sending text search results to %s", searchParams.ResponseAddr) + + if searchParams.ResponseAddr == "" { + printErr("Error: Response address is empty") return } - printDebug("Received search-music from %s", req.ResponseAddr) - results := fetchMusicResults(req.Query, req.Page) - data, err := encodeMusicResults(results) + err = sendMessage(searchParams.ResponseAddr, responseMsg) if err != nil { - printWarn("Failed to encode music results: %v", err) - return + printWarn("Error sending text search results to %s: %v", searchParams.ResponseAddr, err) } - - sendBinaryResponse(req, MsgTypeSearchMusicResponse, data, msg.ID) -} - -func handleSearchFileMessage(msg Message) { - var req searchParams - if err := json.Unmarshal([]byte(msg.Content), &req); err != nil { - printWarn("Invalid JSON: %v", err) - return - } - printDebug("Received search-file from %s", req.ResponseAddr) - - results := fetchFileResults(req.Query, req.Safe, req.Lang, req.Page) - data, err := encodeFileResults(results) - if err != nil { - printWarn("Failed to encode file results: %v", err) - return - } - - sendBinaryResponse(req, MsgTypeSearchFileResponse, data, msg.ID) -} - -func handleSearchForumMessage(msg Message) { - var req searchParams - if err := json.Unmarshal([]byte(msg.Content), &req); err != nil { - printWarn("Invalid JSON: %v", err) - return - } - printDebug("Received search-forum from %s", req.ResponseAddr) - - results := fetchForumResults(req.Query, req.Safe, req.Lang, req.Page) - data, err := encodeForumResults(results) - if err != nil { - printWarn("Failed to encode forum results: %v", err) - return - } - - sendBinaryResponse(req, MsgTypeSearchForumResponse, data, msg.ID) } func handleSearchImageMessage(msg Message) { - var req searchParams - if err := json.Unmarshal([]byte(msg.Content), &req); err != nil { - printWarn("Invalid JSON: %v", err) - return + var searchParams struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` } - printDebug("Received image search type %d from %s", msg.Type, req.ResponseAddr) - - var ( - thumbsNeeded bool - fullNeeded bool - ) - - switch msg.Type { - case MsgTypeSearchImageRawRequest: - thumbsNeeded = false - fullNeeded = false - case MsgTypeSearchImageThumbRequest: - thumbsNeeded = true - fullNeeded = false - case MsgTypeSearchImageFullRequest: - thumbsNeeded = false - fullNeeded = true - case MsgTypeSearchImageAllRequest: - thumbsNeeded = true - fullNeeded = true - default: - printWarn("Unknown image search type: %d", msg.Type) - return - } - - results := fetchImageResults(req.Query, req.Safe, req.Lang, req.Page, true, thumbsNeeded) - - if fullNeeded || thumbsNeeded { - results = prepareProxiedImages(results, msg.Type) - } - - data, err := encodeImageResults(results) + err := json.Unmarshal([]byte(msg.Content), &searchParams) if err != nil { - printWarn("Failed to encode image results: %v", err) + log.Printf("Error parsing search parameters: %v", err) return } - sendBinaryResponse(req, MsgTypeSearchImageResponse, data, msg.ID) + log.Printf("Received search-image request. ResponseAddr: %s", searchParams.ResponseAddr) + results := fetchImageResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page, true) + resultsJSON, err := json.Marshal(results) + if err != nil { + log.Printf("Error marshalling search results: %v", err) + return + } + + responseMsg := Message{ + ID: hostID, + Type: "image-results", + Content: string(resultsJSON), + } + + // Log the address to be used for sending the response + log.Printf("Sending image search results to %s", searchParams.ResponseAddr) + + if searchParams.ResponseAddr == "" { + log.Printf("Error: Response address is empty") + return + } + + err = sendMessage(searchParams.ResponseAddr, responseMsg) + if err != nil { + log.Printf("Error sending image search results to %s: %v", searchParams.ResponseAddr, err) + } +} + +func handleSearchVideoMessage(msg Message) { + var searchParams struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` + } + err := json.Unmarshal([]byte(msg.Content), &searchParams) + if err != nil { + log.Printf("Error parsing search parameters: %v", err) + return + } + + log.Printf("Received search-video request. ResponseAddr: %s", searchParams.ResponseAddr) + + results := fetchVideoResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) + resultsJSON, err := json.Marshal(results) + if err != nil { + log.Printf("Error marshalling search results: %v", err) + return + } + + responseMsg := Message{ + ID: hostID, + Type: "video-results", + Content: string(resultsJSON), + } + + log.Printf("Sending video search results to %s", searchParams.ResponseAddr) + + if searchParams.ResponseAddr == "" { + log.Printf("Error: Response address is empty") + return + } + + err = sendMessage(searchParams.ResponseAddr, responseMsg) + if err != nil { + log.Printf("Error sending video search results to %s: %v", searchParams.ResponseAddr, err) + } +} + +func handleSearchFileMessage(msg Message) { + var searchParams struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` + } + err := json.Unmarshal([]byte(msg.Content), &searchParams) + if err != nil { + log.Printf("Error parsing search parameters: %v", err) + return + } + + log.Printf("Received search-file request. ResponseAddr: %s", searchParams.ResponseAddr) + + results := fetchFileResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) + resultsJSON, err := json.Marshal(results) + if err != nil { + log.Printf("Error marshalling search results: %v", err) + return + } + + responseMsg := Message{ + ID: hostID, + Type: "file-results", + Content: string(resultsJSON), + } + + log.Printf("Sending file search results to %s", searchParams.ResponseAddr) + + if searchParams.ResponseAddr == "" { + log.Printf("Error: Response address is empty") + return + } + + err = sendMessage(searchParams.ResponseAddr, responseMsg) + if err != nil { + log.Printf("Error sending file search results to %s: %v", searchParams.ResponseAddr, err) + } +} + +func handleSearchForumMessage(msg Message) { + var searchParams struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` + } + err := json.Unmarshal([]byte(msg.Content), &searchParams) + if err != nil { + log.Printf("Error parsing search parameters: %v", err) + return + } + + log.Printf("Received search-forum request. ResponseAddr: %s", searchParams.ResponseAddr) + + results := fetchForumResults(searchParams.Query, searchParams.Safe, searchParams.Lang, searchParams.Page) + resultsJSON, err := json.Marshal(results) + if err != nil { + log.Printf("Error marshalling search results: %v", err) + return + } + + responseMsg := Message{ + ID: hostID, + Type: "forum-results", + Content: string(resultsJSON), + } + + // Log the address to be used for sending the response + log.Printf("Sending forum search results to %s", searchParams.ResponseAddr) + + if searchParams.ResponseAddr == "" { + log.Printf("Error: Response address is empty") + return + } + + err = sendMessage(searchParams.ResponseAddr, responseMsg) + if err != nil { + log.Printf("Error sending forum search results to %s: %v", searchParams.ResponseAddr, err) + } } diff --git a/node-master.go b/node-master.go new file mode 100644 index 0000000..133f72e --- /dev/null +++ b/node-master.go @@ -0,0 +1,91 @@ +package main + +import ( + "log" + "sync" + "time" +) + +var ( + isMaster bool + masterNode string + masterNodeMux sync.RWMutex +) + +const ( + heartbeatInterval = 5 * time.Second + heartbeatTimeout = 15 * time.Second + electionTimeout = 10 * time.Second +) + +func sendHeartbeats() { + for { + if !isMaster { + return + } + for _, node := range peers { + msg := Message{ + ID: hostID, + Type: "heartbeat", + Content: authCode, + } + err := sendMessage(node, msg) + if err != nil { + log.Printf("Error sending heartbeat to %s: %v", node, err) + } + } + time.Sleep(heartbeatInterval) + } +} + +func checkMasterHeartbeat() { + for { + time.Sleep(heartbeatTimeout) + masterNodeMux.RLock() + if masterNode == authCode || masterNode == "" { + masterNodeMux.RUnlock() + continue + } + masterNodeMux.RUnlock() + + masterNodeMux.Lock() + masterNode = "" + masterNodeMux.Unlock() + startElection() + } +} + +func startElection() { + masterNodeMux.Lock() + defer masterNodeMux.Unlock() + + for _, node := range peers { + msg := Message{ + ID: hostID, + Type: "election", + Content: authCode, + } + err := sendMessage(node, msg) + if err != nil { + log.Printf("Error sending election message to %s: %v", node, err) + } + } + + isMaster = true + go sendHeartbeats() +} + +func handleHeartbeat(content string) { + masterNodeMux.Lock() + defer masterNodeMux.Unlock() + masterNode = content +} + +func handleElection(content string) { + masterNodeMux.Lock() + defer masterNodeMux.Unlock() + + if content < authCode { + masterNode = content + } +} diff --git a/node-request-files.go b/node-request-files.go index 2153850..0cabf32 100755 --- a/node-request-files.go +++ b/node-request-files.go @@ -1,22 +1,19 @@ -//go:build experimental -// +build experimental - package main import ( - "bytes" - "encoding/binary" + "encoding/json" "fmt" "time" ) -var fileResultsChan = make(chan []TorrentResult) - -func tryOtherNodesForFileSearch(query, safe, lang string, page int) []TorrentResult { - for _, nodeTarget := range sockets { - results, err := sendFileSearchRequestToNode(nodeTarget, query, safe, lang, page) +func tryOtherNodesForFileSearch(query, safe, lang string, page int, visitedNodes []string) []TorrentResult { + for _, nodeAddr := range peers { + if contains(visitedNodes, nodeAddr) { + continue // Skip nodes already visited + } + results, err := sendFileSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes) if err != nil { - printWarn("Error contacting node %s: %v", nodeTarget, err) + printWarn("Error contacting node %s: %v", nodeAddr, err) continue } if len(results) > 0 { @@ -26,123 +23,60 @@ func tryOtherNodesForFileSearch(query, safe, lang string, page int) []TorrentRes return nil } -func sendFileSearchRequestToNode(target, query, safe, lang string, page int) ([]TorrentResult, error) { - payload, err := encodeSearchTextParams(query, safe, lang, page) +func sendFileSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]TorrentResult, error) { + visitedNodes = append(visitedNodes, nodeAddr) + searchParams := struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` + VisitedNodes []string `json:"visitedNodes"` + }{ + Query: query, + Safe: safe, + Lang: lang, + Page: page, + ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), + VisitedNodes: visitedNodes, + } + + msgBytes, err := json.Marshal(searchParams) if err != nil { - return nil, fmt.Errorf("encode error: %v", err) + return nil, fmt.Errorf("failed to marshal search parameters: %v", err) } msg := Message{ - ID: generateMessageID(), - Type: MsgTypeSearchFileRequest, - Content: payload, - Target: target, + ID: hostID, + Type: "search-file", + Content: string(msgBytes), } - if err := sendMessage(msg); err != nil { - return nil, fmt.Errorf("send error: %v", err) + err = sendMessage(nodeAddr, msg) + if err != nil { + return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) } + // Wait for results select { case res := <-fileResultsChan: return res, nil case <-time.After(20 * time.Second): - return nil, fmt.Errorf("timeout waiting for results from node %s", target) + return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) } } func handleFileResultsMessage(msg Message) { - results, err := decodeFileResults([]byte(msg.Content)) + var results []TorrentResult + err := json.Unmarshal([]byte(msg.Content), &results) if err != nil { - printWarn("Error decoding file results: %v", err) + printWarn("Error unmarshalling file results: %v", err) return } - printDebug("Received file results: %+v", results) + printDebug("Received file results: %+v", results) + // Send results to fileResultsChan go func() { fileResultsChan <- results }() } - -func encodeFileResults(results []TorrentResult) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := binary.Write(buf, binary.BigEndian, uint16(len(results))); err != nil { - return nil, err - } - - for _, r := range results { - if err := writeString(buf, r.URL); err != nil { - return nil, err - } - if err := binary.Write(buf, binary.BigEndian, uint32(r.Seeders)); err != nil { - return nil, err - } - if err := binary.Write(buf, binary.BigEndian, uint32(r.Leechers)); err != nil { - return nil, err - } - if err := writeString(buf, r.Magnet); err != nil { - return nil, err - } - if err := binary.Write(buf, binary.BigEndian, uint32(r.Views)); err != nil { - return nil, err - } - if err := writeString(buf, r.Size); err != nil { - return nil, err - } - if err := writeString(buf, r.Title); err != nil { - return nil, err - } - } - return buf.Bytes(), nil -} - -func decodeFileResults(data []byte) ([]TorrentResult, error) { - buf := bytes.NewReader(data) - - var count uint16 - if err := binary.Read(buf, binary.BigEndian, &count); err != nil { - return nil, err - } - - results := make([]TorrentResult, 0, count) - for i := 0; i < int(count); i++ { - url, err := readString(buf) - if err != nil { - return nil, err - } - var seeders, leechers, views uint32 - if err := binary.Read(buf, binary.BigEndian, &seeders); err != nil { - return nil, err - } - if err := binary.Read(buf, binary.BigEndian, &leechers); err != nil { - return nil, err - } - magnet, err := readString(buf) - if err != nil { - return nil, err - } - if err := binary.Read(buf, binary.BigEndian, &views); err != nil { - return nil, err - } - size, err := readString(buf) - if err != nil { - return nil, err - } - title, err := readString(buf) - if err != nil { - return nil, err - } - - results = append(results, TorrentResult{ - URL: url, - Seeders: int(seeders), - Leechers: int(leechers), - Magnet: magnet, - Views: int(views), - Size: size, - Title: title, - }) - } - return results, nil -} diff --git a/node-request-forums.go b/node-request-forums.go index 4f248ce..ff6ed2e 100755 --- a/node-request-forums.go +++ b/node-request-forums.go @@ -1,129 +1,100 @@ -//go:build experimental -// +build experimental - package main import ( - "bytes" - "encoding/binary" + "encoding/json" "fmt" "time" ) var forumResultsChan = make(chan []ForumSearchResult) -func sendForumSearchRequestToNode(target, query, safe, lang string, page int, visitedNodes []string) ([]ForumSearchResult, error) { - payload, err := encodeSearchTextParams(query, safe, lang, page) // reuse text param encoding +func tryOtherNodesForForumSearch(query, safe, lang string, page int) []ForumSearchResult { + for _, nodeAddr := range peers { + results, err := sendForumSearchRequestToNode(nodeAddr, query, safe, lang, page, []string{}) + if err != nil { + printWarn("Error contacting node %s: %v", nodeAddr, err) + continue + } + if len(results) > 0 { + return results + } + } + return nil +} + +func sendForumSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]ForumSearchResult, error) { + // Check if the current node has already been visited + for _, node := range visitedNodes { + if node == hostID { + return nil, fmt.Errorf("loop detected: this node (%s) has already been visited", hostID) + } + } + + // Add current node to the list of visited nodes + visitedNodes = append(visitedNodes, hostID) + + searchParams := struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` + VisitedNodes []string `json:"visitedNodes"` + }{ + Query: query, + Safe: safe, + Lang: lang, + Page: page, + ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), + VisitedNodes: visitedNodes, + } + + msgBytes, err := json.Marshal(searchParams) if err != nil { - return nil, fmt.Errorf("encode error: %v", err) + return nil, fmt.Errorf("failed to marshal search parameters: %v", err) } msg := Message{ - ID: generateMessageID(), - Type: MsgTypeSearchForumRequest, - Content: payload, - Target: target, + ID: hostID, + Type: "search-forum", + Content: string(msgBytes), } - if err := sendMessage(msg); err != nil { - return nil, fmt.Errorf("send error: %v", err) + err = sendMessage(nodeAddr, msg) + if err != nil { + return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) } + // Wait for results select { case res := <-forumResultsChan: return res, nil case <-time.After(20 * time.Second): - return nil, fmt.Errorf("timeout waiting for results from node %s", target) + return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) } } func handleForumResultsMessage(msg Message) { - results, err := decodeForumResults([]byte(msg.Content)) + var results []ForumSearchResult + err := json.Unmarshal([]byte(msg.Content), &results) if err != nil { - printWarn("Error decoding forum results: %v", err) + printWarn("Error unmarshalling forum results: %v", err) return } - printDebug("Received forum results: %+v", results) + printDebug("Received forum results: %+v", results) + // Send results to forumResultsChan go func() { forumResultsChan <- results }() } -func encodeForumResults(results []ForumSearchResult) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := binary.Write(buf, binary.BigEndian, uint16(len(results))); err != nil { - return nil, err +// Used only to answer requests +func fetchForumResults(query, safe, lang string, page int) []ForumSearchResult { + results, err := PerformRedditSearch(query, safe, page) + if err != nil { + printWarn("Error fetching forum results: %v", err) + return nil } - - for _, r := range results { - if err := writeString(buf, r.URL); err != nil { - return nil, err - } - if err := writeString(buf, r.Header); err != nil { - return nil, err - } - if err := writeString(buf, r.Description); err != nil { - return nil, err - } - if err := binary.Write(buf, binary.BigEndian, r.PublishedDate.Unix()); err != nil { - return nil, err - } - if err := writeString(buf, r.ImgSrc); err != nil { - return nil, err - } - if err := writeString(buf, r.ThumbnailSrc); err != nil { - return nil, err - } - } - - return buf.Bytes(), nil -} - -func decodeForumResults(data []byte) ([]ForumSearchResult, error) { - buf := bytes.NewReader(data) - - var count uint16 - if err := binary.Read(buf, binary.BigEndian, &count); err != nil { - return nil, err - } - - results := make([]ForumSearchResult, 0, count) - for i := 0; i < int(count); i++ { - url, err := readString(buf) - if err != nil { - return nil, err - } - header, err := readString(buf) - if err != nil { - return nil, err - } - desc, err := readString(buf) - if err != nil { - return nil, err - } - var unixTime int64 - if err := binary.Read(buf, binary.BigEndian, &unixTime); err != nil { - return nil, err - } - imgSrc, err := readString(buf) - if err != nil { - return nil, err - } - thumbSrc, err := readString(buf) - if err != nil { - return nil, err - } - - results = append(results, ForumSearchResult{ - URL: url, - Header: header, - Description: desc, - PublishedDate: time.Unix(unixTime, 0), - ImgSrc: imgSrc, - ThumbnailSrc: thumbSrc, - }) - } - return results, nil + return results } diff --git a/node-request-images.go b/node-request-images.go index db980c1..4e3e9e3 100755 --- a/node-request-images.go +++ b/node-request-images.go @@ -1,198 +1,84 @@ -//go:build experimental -// +build experimental - package main import ( - "bytes" - "encoding/binary" + "encoding/json" "fmt" "time" ) -// const ( -// MessageTypeSearchImage uint8 = 11 -// MessageTypeSearchImageThumb uint8 = 111 -// MessageTypeSearchImageFull uint8 = 112 -// MessageTypeSearchImageAllProxy uint8 = 113 -// MessageTypeImageResults uint8 = 22 -// ) - var imageResultsChan = make(chan []ImageSearchResult) -func sendImageSearchRequestToNode(target, query, safe, lang string, page int, mode uint8) ([]ImageSearchResult, error) { - payload, err := encodeSearchTextParams(query, safe, lang, page) - if err != nil { - return nil, fmt.Errorf("encode error: %v", err) - } - - msg := Message{ - ID: generateMessageID(), - Type: mode, // one of the image search types - Content: payload, - Target: target, - } - - if err := sendMessage(msg); err != nil { - return nil, fmt.Errorf("send error: %v", err) - } - - select { - case res := <-imageResultsChan: - return res, nil - case <-time.After(30 * time.Second): - return nil, fmt.Errorf("timeout from %s", target) - } -} - func handleImageResultsMessage(msg Message) { - results, err := decodeImageResults([]byte(msg.Content)) + var results []ImageSearchResult + err := json.Unmarshal([]byte(msg.Content), &results) if err != nil { - printWarn("Error decoding image results: %v", err) + printWarn("Error unmarshalling image results: %v", err) return } - printDebug("Received image results: %+v", results) + printDebug("Received image results: %+v", results) + // Send results to imageResultsChan go func() { imageResultsChan <- results }() } -func encodeImageResults(results []ImageSearchResult) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := binary.Write(buf, binary.BigEndian, uint16(len(results))); err != nil { - return nil, err +func sendImageSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]ImageSearchResult, error) { + visitedNodes = append(visitedNodes, nodeAddr) + searchParams := struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` + VisitedNodes []string `json:"visitedNodes"` + }{ + Query: query, + Safe: safe, + Lang: lang, + Page: page, + ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), + VisitedNodes: visitedNodes, } - for _, r := range results { - if err := writeString(buf, r.ID); err != nil { - return nil, err - } - if err := writeString(buf, r.Title); err != nil { - return nil, err - } - if err := writeString(buf, r.Full); err != nil { - return nil, err - } - if err := writeString(buf, r.Thumb); err != nil { - return nil, err - } - if err := writeString(buf, r.ProxyFull); err != nil { - return nil, err - } - if err := writeString(buf, r.ProxyThumb); err != nil { - return nil, err - } - if err := writeString(buf, r.Source); err != nil { - return nil, err - } - if err := binary.Write(buf, binary.BigEndian, int32(r.Width)); err != nil { - return nil, err - } - if err := binary.Write(buf, binary.BigEndian, int32(r.Height)); err != nil { - return nil, err - } + msgBytes, err := json.Marshal(searchParams) + if err != nil { + return nil, fmt.Errorf("failed to marshal search parameters: %v", err) } - return buf.Bytes(), nil + msg := Message{ + ID: hostID, + Type: "search-image", + Content: string(msgBytes), + } + + err = sendMessage(nodeAddr, msg) + if err != nil { + return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) + } + + // Wait for results + select { + case res := <-imageResultsChan: + return res, nil + case <-time.After(30 * time.Second): + return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) + } } -func decodeImageResults(data []byte) ([]ImageSearchResult, error) { - buf := bytes.NewReader(data) - - var count uint16 - if err := binary.Read(buf, binary.BigEndian, &count); err != nil { - return nil, err - } - - results := make([]ImageSearchResult, 0, count) - for i := 0; i < int(count); i++ { - id, _ := readString(buf) - title, _ := readString(buf) - full, _ := readString(buf) - thumb, _ := readString(buf) - proxyFull, _ := readString(buf) - proxyThumb, _ := readString(buf) - source, _ := readString(buf) - - var width, height int32 - if err := binary.Read(buf, binary.BigEndian, &width); err != nil { - return nil, err +func tryOtherNodesForImageSearch(query, safe, lang string, page int, visitedNodes []string) []ImageSearchResult { + for _, nodeAddr := range peers { + if contains(visitedNodes, nodeAddr) { + continue // Skip nodes already visited } - if err := binary.Read(buf, binary.BigEndian, &height); err != nil { - return nil, err + results, err := sendImageSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes) + if err != nil { + printWarn("Error contacting node %s: %v", nodeAddr, err) + continue + } + if len(results) > 0 { + return results } - - results = append(results, ImageSearchResult{ - ID: id, - Title: title, - Full: full, - Thumb: thumb, - ProxyFull: proxyFull, - ProxyThumb: proxyThumb, - Source: source, - Width: int(width), - Height: int(height), - }) } - - return results, nil + return nil } - -func prepareProxiedImages(results []ImageSearchResult, mode uint8) []ImageSearchResult { - for i := range results { - switch mode { - case MsgTypeSearchImageThumbRequest: - results[i].ProxyThumb = "/image/" + results[i].ID + "_thumb.webp" - case MsgTypeSearchImageFullRequest: - results[i].ProxyFull = "/image/" + results[i].ID + "_full.webp" - case MsgTypeSearchImageAllRequest: - results[i].ProxyThumb = "/image/" + results[i].ID + "_thumb.webp" - results[i].ProxyFull = "/image/" + results[i].ID + "_full.webp" - } - } - return results -} - -// func handleSearchImageMessage(msg Message) { -// query, safe, lang, page, err := decodeSearchTextParams([]byte(msg.Content)) -// if err != nil { -// printWarn("Error decoding image search parameters: %v", err) -// return -// } - -// results := fetchImageResults(query, safe, lang, page, true, true) - -// switch msg.Type { -// case MsgTypeSearchImageRawRequest: -// // No proxy URLs needed - -// case MsgTypeSearchImageThumbRequest: -// results = prepareProxiedImages(results, MsgTypeSearchImageThumbRequest) -// case MsgTypeSearchImageFullRequest: -// results = prepareProxiedImages(results, MsgTypeSearchImageFullRequest) -// case MsgTypeSearchImageAllRequest: -// results = prepareProxiedImages(results, MsgTypeSearchImageAllRequest) -// default: -// printWarn("Unknown image request mode: %d", msg.Type) -// return -// } - -// payload, err := encodeImageResults(results) -// if err != nil { -// printWarn("Error encoding image search results: %v", err) -// return -// } - -// response := Message{ -// ID: msg.ID, -// Type: MsgTypeSearchImageResponse, -// Content: string(payload), -// Target: msg.Source, // Reply to sender -// } - -// if err := sendMessage(response); err != nil { -// printWarn("Error sending image search response: %v", err) -// } -// } diff --git a/node-request-music.go b/node-request-music.go deleted file mode 100644 index e7d4961..0000000 --- a/node-request-music.go +++ /dev/null @@ -1,187 +0,0 @@ -//go:build experimental -// +build experimental - -package main - -import ( - "bytes" - "encoding/binary" - "fmt" - "time" -) - -var musicResultsChan = make(chan []MusicResult) - -func tryOtherNodesForMusicSearch(query, lang string, safe bool, page int) []MusicResult { - safeStr := "inactive" - if safe { - safeStr = "active" - } - for _, nodeTarget := range sockets { - results, err := sendMusicSearchRequestToNode(nodeTarget, query, safeStr, lang, page) - if err != nil { - printWarn("Error contacting node %s: %v", nodeTarget, err) - continue - } - if len(results) > 0 { - return results - } - } - return nil -} - -func sendMusicSearchRequestToNode(target, query, safe, lang string, page int) ([]MusicResult, error) { - payload, err := encodeSearchTextParams(query, safe, lang, page) - if err != nil { - return nil, fmt.Errorf("encode error: %v", err) - } - - msg := Message{ - ID: generateMessageID(), - Type: MsgTypeSearchMusicRequest, - Content: payload, - Target: target, - } - - err = sendMessage(msg) - if err != nil { - return nil, fmt.Errorf("failed to send music request to node %s: %v", target, err) - } - - select { - case res := <-musicResultsChan: - return res, nil - case <-time.After(20 * time.Second): - return nil, fmt.Errorf("timeout waiting for music results from node %s", target) - } -} - -func handleMusicResultsMessage(msg Message) { - results, err := decodeMusicResults([]byte(msg.Content)) - if err != nil { - printWarn("Error decoding music results: %v", err) - return - } - printDebug("Received music results: %+v", results) - - go func() { - musicResultsChan <- results - }() -} - -func encodeMusicResults(results []MusicResult) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := binary.Write(buf, binary.BigEndian, uint16(len(results))); err != nil { - return nil, err - } - - for _, r := range results { - if err := writeString(buf, r.URL); err != nil { - return nil, err - } - if err := writeString(buf, r.Title); err != nil { - return nil, err - } - if err := writeString(buf, r.Artist); err != nil { - return nil, err - } - if err := writeString(buf, r.Description); err != nil { - return nil, err - } - if err := writeString(buf, r.PublishedDate); err != nil { - return nil, err - } - if err := writeString(buf, r.Thumbnail); err != nil { - return nil, err - } - if err := writeString(buf, r.Source); err != nil { - return nil, err - } - if err := writeString(buf, r.Duration); err != nil { - return nil, err - } - } - - return buf.Bytes(), nil -} - -func decodeMusicResults(data []byte) ([]MusicResult, error) { - buf := bytes.NewReader(data) - - var count uint16 - if err := binary.Read(buf, binary.BigEndian, &count); err != nil { - return nil, err - } - - results := make([]MusicResult, 0, count) - for i := 0; i < int(count); i++ { - url, err := readString(buf) - if err != nil { - return nil, err - } - title, err := readString(buf) - if err != nil { - return nil, err - } - artist, err := readString(buf) - if err != nil { - return nil, err - } - description, err := readString(buf) - if err != nil { - return nil, err - } - date, err := readString(buf) - if err != nil { - return nil, err - } - thumb, err := readString(buf) - if err != nil { - return nil, err - } - source, err := readString(buf) - if err != nil { - return nil, err - } - duration, err := readString(buf) - if err != nil { - return nil, err - } - - results = append(results, MusicResult{ - URL: url, - Title: title, - Artist: artist, - Description: description, - PublishedDate: date, - Thumbnail: thumb, - Source: source, - Duration: duration, - }) - } - return results, nil -} - -// func handleMusicSearchRequest(msg Message) { -// buf := bytes.NewReader([]byte(msg.Content)) -// query, _ := readString(buf) - -// var page uint16 -// binary.Read(buf, binary.BigEndian, &page) - -// results := fetchMusicResults(query, int(page)) -// encoded, err := encodeMusicResults(results) -// if err != nil { -// printWarn("Encoding music results failed: %v", err) -// return -// } - -// reply := Message{ -// ID: msg.ID, -// Type: MsgTypeSearchMusicResponse, -// Content: string(encoded), -// Target: msg.Target, // Send back to sender -// } -// sendMessage(reply) -// } diff --git a/node-request-text.go b/node-request-text.go index ad9bacf..ebe6041 100755 --- a/node-request-text.go +++ b/node-request-text.go @@ -1,23 +1,21 @@ -//go:build experimental -// +build experimental - package main import ( - "bytes" - "encoding/binary" + "encoding/json" "fmt" "time" ) var textResultsChan = make(chan []TextSearchResult) -// Try other nodes is not defined for every type -func tryOtherNodesForTextSearch(query, safe, lang string, page int) []TextSearchResult { - for _, nodeTarget := range sockets { - results, err := sendTextSearchRequestToNode(nodeTarget, query, safe, lang, page) +func tryOtherNodesForTextSearch(query, safe, lang string, page int, visitedNodes []string) []TextSearchResult { + for _, nodeAddr := range peers { + if contains(visitedNodes, nodeAddr) { + continue // Skip nodes already visited + } + results, err := sendTextSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes) if err != nil { - printWarn("Error contacting node %s: %v", nodeTarget, err) + printWarn("Error contacting node %s: %v", nodeAddr, err) continue } if len(results) > 0 { @@ -27,102 +25,60 @@ func tryOtherNodesForTextSearch(query, safe, lang string, page int) []TextSearch return nil } -func sendTextSearchRequestToNode(target, query, safe, lang string, page int) ([]TextSearchResult, error) { - payload, err := encodeSearchTextParams(query, safe, lang, page) +func sendTextSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]TextSearchResult, error) { + visitedNodes = append(visitedNodes, nodeAddr) + searchParams := struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` + VisitedNodes []string `json:"visitedNodes"` + }{ + Query: query, + Safe: safe, + Lang: lang, + Page: page, + ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), + VisitedNodes: visitedNodes, + } + + msgBytes, err := json.Marshal(searchParams) if err != nil { - return nil, fmt.Errorf("encode error: %v", err) + return nil, fmt.Errorf("failed to marshal search parameters: %v", err) } msg := Message{ - ID: generateMessageID(), // assume function returns uint32 - Type: MsgTypeSearchTextRequest, - Content: payload, - Target: target, + ID: hostID, + Type: "search-text", + Content: string(msgBytes), } - err = sendMessage(msg) + err = sendMessage(nodeAddr, msg) if err != nil { - return nil, fmt.Errorf("failed to send search request to node %s: %v", target, err) + return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) } + // Wait for results select { case res := <-textResultsChan: return res, nil case <-time.After(20 * time.Second): - return nil, fmt.Errorf("timeout waiting for results from node %s", target) + return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) } } func handleTextResultsMessage(msg Message) { - results, err := decodeTextResults([]byte(msg.Content)) + var results []TextSearchResult + err := json.Unmarshal([]byte(msg.Content), &results) if err != nil { - printWarn("Error decoding text results: %v", err) + printWarn("Error unmarshalling text results: %v", err) return } - printDebug("Received text results: %+v", results) + printDebug("Received text results: %+v", results) + // Send results to textResultsChan go func() { textResultsChan <- results }() } - -func encodeTextResults(results []TextSearchResult) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := binary.Write(buf, binary.BigEndian, uint16(len(results))); err != nil { - return nil, err - } - - for _, r := range results { - if err := writeString(buf, r.URL); err != nil { - return nil, err - } - if err := writeString(buf, r.Header); err != nil { - return nil, err - } - if err := writeString(buf, r.Description); err != nil { - return nil, err - } - if err := writeString(buf, r.Source); err != nil { - return nil, err - } - } - - return buf.Bytes(), nil -} - -func decodeTextResults(data []byte) ([]TextSearchResult, error) { - buf := bytes.NewReader(data) - - var count uint16 - if err := binary.Read(buf, binary.BigEndian, &count); err != nil { - return nil, err - } - - results := make([]TextSearchResult, 0, count) - for i := 0; i < int(count); i++ { - url, err := readString(buf) - if err != nil { - return nil, err - } - header, err := readString(buf) - if err != nil { - return nil, err - } - description, err := readString(buf) - if err != nil { - return nil, err - } - source, err := readString(buf) - if err != nil { - return nil, err - } - results = append(results, TextSearchResult{ - URL: url, - Header: header, - Description: description, - Source: source, - }) - } - return results, nil -} diff --git a/node-request-video.go b/node-request-video.go index e213e2c..d965a7d 100755 --- a/node-request-video.go +++ b/node-request-video.go @@ -1,22 +1,19 @@ -//go:build experimental -// +build experimental - package main import ( - "bytes" - "encoding/binary" + "encoding/json" "fmt" "time" ) -var videoResultsChan = make(chan []VideoResult) - -func tryOtherNodesForVideoSearch(query, safe, lang string, page int) []VideoResult { - for _, node := range sockets { - results, err := sendVideoSearchRequestToNode(node, query, safe, lang, page) +func tryOtherNodesForVideoSearch(query, safe, lang string, page int, visitedNodes []string) []VideoResult { + for _, nodeAddr := range peers { + if contains(visitedNodes, nodeAddr) { + continue // Skip nodes already visited + } + results, err := sendVideoSearchRequestToNode(nodeAddr, query, safe, lang, page, visitedNodes) if err != nil { - printWarn("Error contacting node %s: %v", node, err) + printWarn("Error contacting node %s: %v", nodeAddr, err) continue } if len(results) > 0 { @@ -26,134 +23,60 @@ func tryOtherNodesForVideoSearch(query, safe, lang string, page int) []VideoResu return nil } -func sendVideoSearchRequestToNode(target, query, safe, lang string, page int) ([]VideoResult, error) { - payload, err := encodeSearchTextParams(query, safe, lang, page) +func sendVideoSearchRequestToNode(nodeAddr, query, safe, lang string, page int, visitedNodes []string) ([]VideoResult, error) { + visitedNodes = append(visitedNodes, nodeAddr) + searchParams := struct { + Query string `json:"query"` + Safe string `json:"safe"` + Lang string `json:"lang"` + Page int `json:"page"` + ResponseAddr string `json:"responseAddr"` + VisitedNodes []string `json:"visitedNodes"` + }{ + Query: query, + Safe: safe, + Lang: lang, + Page: page, + ResponseAddr: fmt.Sprintf("http://localhost:%d/node", config.Port), + VisitedNodes: visitedNodes, + } + + msgBytes, err := json.Marshal(searchParams) if err != nil { - return nil, fmt.Errorf("encode error: %v", err) + return nil, fmt.Errorf("failed to marshal search parameters: %v", err) } msg := Message{ - ID: generateMessageID(), - Type: MsgTypeSearchVideoRequest, - Content: payload, - Target: target, + ID: hostID, + Type: "search-video", + Content: string(msgBytes), } - if err := sendMessage(msg); err != nil { - return nil, fmt.Errorf("send error: %v", err) + err = sendMessage(nodeAddr, msg) + if err != nil { + return nil, fmt.Errorf("failed to send search request to node %s: %v", nodeAddr, err) } + // Wait for results select { case res := <-videoResultsChan: return res, nil case <-time.After(20 * time.Second): - return nil, fmt.Errorf("timeout waiting for results from node %s", target) + return nil, fmt.Errorf("timeout waiting for results from node %s", nodeAddr) } } func handleVideoResultsMessage(msg Message) { - results, err := decodeVideoResults([]byte(msg.Content)) + var results []VideoResult + err := json.Unmarshal([]byte(msg.Content), &results) if err != nil { - printWarn("Error decoding video results: %v", err) + printWarn("Error unmarshalling video results: %v", err) return } - printDebug("Received video results: %+v", results) + printDebug("Received video results: %+v", results) + // Send results to videoResultsChan go func() { videoResultsChan <- results }() } - -func encodeVideoResults(results []VideoResult) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := binary.Write(buf, binary.BigEndian, uint16(len(results))); err != nil { - return nil, err - } - - for _, r := range results { - if err := writeString(buf, r.Href); err != nil { - return nil, err - } - if err := writeString(buf, r.Title); err != nil { - return nil, err - } - if err := writeString(buf, r.Date); err != nil { - return nil, err - } - if err := writeString(buf, r.Views); err != nil { - return nil, err - } - if err := writeString(buf, r.Creator); err != nil { - return nil, err - } - if err := writeString(buf, r.Publisher); err != nil { - return nil, err - } - if err := writeString(buf, r.Image); err != nil { - return nil, err - } - if err := writeString(buf, r.Duration); err != nil { - return nil, err - } - } - - return buf.Bytes(), nil -} - -func decodeVideoResults(data []byte) ([]VideoResult, error) { - buf := bytes.NewReader(data) - - var count uint16 - if err := binary.Read(buf, binary.BigEndian, &count); err != nil { - return nil, err - } - - results := make([]VideoResult, 0, count) - for i := 0; i < int(count); i++ { - href, err := readString(buf) - if err != nil { - return nil, err - } - title, err := readString(buf) - if err != nil { - return nil, err - } - date, err := readString(buf) - if err != nil { - return nil, err - } - views, err := readString(buf) - if err != nil { - return nil, err - } - creator, err := readString(buf) - if err != nil { - return nil, err - } - publisher, err := readString(buf) - if err != nil { - return nil, err - } - image, err := readString(buf) - if err != nil { - return nil, err - } - duration, err := readString(buf) - if err != nil { - return nil, err - } - - results = append(results, VideoResult{ - Href: href, - Title: title, - Date: date, - Views: views, - Creator: creator, - Publisher: publisher, - Image: image, - Duration: duration, - }) - } - return results, nil -} diff --git a/node-update.go b/node-update.go new file mode 100644 index 0000000..f433eb4 --- /dev/null +++ b/node-update.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "log" + "time" +) + +// Function to sync updates across all nodes +func nodeUpdateSync() { + fmt.Println("Syncing updates across all nodes...") + for _, peerAddr := range peers { + fmt.Printf("Notifying node %s about update...\n", peerAddr) + msg := Message{ + ID: hostID, + Type: "update", + Content: "Start update process", + } + err := sendMessage(peerAddr, msg) + if err != nil { + log.Printf("Failed to notify node %s: %v\n", peerAddr, err) + continue + } + fmt.Printf("Node %s notified. Waiting for it to update...\n", peerAddr) + time.Sleep(30 * time.Second) // Adjust sleep time as needed to allow for updates + } + fmt.Println("All nodes have been updated.") +} diff --git a/node.go b/node.go index 5b70063..5fd247a 100644 --- a/node.go +++ b/node.go @@ -1,152 +1,75 @@ -//go:build experimental -// +build experimental - package main import ( "bytes" - "encoding/binary" + "crypto/rand" "encoding/json" - "errors" "fmt" - "io" - "net" + "io/ioutil" "net/http" - "os" "time" ) var ( - sockets []string - hostID string - socketDir string + authCode string + peers []string + hostID string ) type Message struct { - ID uint32 - Type uint8 - Content []byte - Target string + ID string `json:"id"` + Type string `json:"type"` + Content string `json:"content"` + VisitedNodes []string `json:"visitedNodes"` } -const ( - MsgTypeNone uint8 = 0 - MsgTypeTest uint8 = 1 - - // Request types (10–99) - MsgTypeSearchTextRequest uint8 = 10 - MsgTypeSearchImageRawRequest uint8 = 11 - MsgTypeSearchImageThumbRequest uint8 = 12 - MsgTypeSearchImageFullRequest uint8 = 13 - MsgTypeSearchImageAllRequest uint8 = 14 - MsgTypeSearchVideoRequest uint8 = 15 - MsgTypeSearchFileRequest uint8 = 16 - MsgTypeSearchForumRequest uint8 = 17 - MsgTypeSearchMusicRequest uint8 = 18 - - // Response types (110–199) - MsgTypeSearchTextResponse uint8 = 110 - MsgTypeSearchImageResponse uint8 = 111 - MsgTypeSearchVideoResponse uint8 = 112 - MsgTypeSearchFileResponse uint8 = 113 - MsgTypeSearchForumResponse uint8 = 114 - MsgTypeSearchMusicResponse uint8 = 115 -) - func loadNodeConfig() { - sockets = config.Nodes - socketDir = "/tmp/" // Directory where sockets are stored, for now fixed tmp dir, can be changed later + authCode = config.AuthCode + peers = config.Peers } -var messageIDCounter uint32 = 0 - -func generateMessageID() uint32 { - if messageIDCounter == ^uint32(0) { // 0xFFFFFFFF - messageIDCounter = 1 - } else { - messageIDCounter++ - } - return messageIDCounter -} - -func encodeSearchTextParams(query, safe, lang string, page int) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := writeString(buf, query); err != nil { - return nil, err - } - if err := writeString(buf, safe); err != nil { - return nil, err - } - if err := writeString(buf, lang); err != nil { - return nil, err - } - if err := binary.Write(buf, binary.BigEndian, uint16(page)); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -func sendMessage(msg Message) error { - socketPath := socketDir + msg.Target + ".sock" - - conn, err := net.Dial("unix", socketPath) +func generateHostID() (string, error) { + bytes := make([]byte, 16) + _, err := rand.Read(bytes) if err != nil { - return fmt.Errorf("failed to connect to socket %s: %v", socketPath, err) + return "", fmt.Errorf("failed to generate host ID: %v", err) } - defer conn.Close() - - msgBytes, err := serializeMessage(msg) - if err != nil { - return fmt.Errorf("serialization error: %v", err) - } - - _, err = conn.Write(msgBytes) - return err + return fmt.Sprintf("%x", bytes), nil } -func startUnixSocketServer(socketName string) { - socketPath := socketDir + socketName + ".sock" - - if _, err := os.Stat(socketPath); err == nil { - os.Remove(socketPath) +func sendMessage(serverAddr string, msg Message) error { + if serverAddr == "" { + return fmt.Errorf("server address is empty") } - listener, err := net.Listen("unix", socketPath) + msgBytes, err := json.Marshal(msg) if err != nil { - panic(fmt.Sprintf("Failed to listen on %s: %v", socketPath, err)) + return fmt.Errorf("failed to marshal message: %v", err) } - defer listener.Close() - os.Chmod(socketPath, 0666) - printInfo("Listening on UNIX socket: %s", socketPath) - - for { - conn, err := listener.Accept() - if err != nil { - printWarn("Accept error: %v", err) - continue - } - - go func(c net.Conn) { - defer c.Close() - buf, err := io.ReadAll(c) - if err != nil { - printWarn("Read error: %v", err) - return - } - - msg, err := deserializeMessage(buf) - if err != nil { - printWarn("Deserialization error: %v", err) - return - } - - printDebug("Received binary message: %+v", msg) - interpretMessage(msg) - }(conn) + req, err := http.NewRequest("POST", serverAddr, bytes.NewBuffer(msgBytes)) + if err != nil { + return fmt.Errorf("failed to create request: %v", err) } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", authCode) + + client := &http.Client{ + Timeout: time.Second * 10, + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to send request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := ioutil.ReadAll(resp.Body) + return fmt.Errorf("server error: %s", body) + } + + return nil } func handleNodeRequest(w http.ResponseWriter, r *http.Request) { @@ -155,6 +78,12 @@ func handleNodeRequest(w http.ResponseWriter, r *http.Request) { return } + auth := r.Header.Get("Authorization") + if auth != authCode { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + var msg Message err := json.NewDecoder(r.Body).Decode(&msg) if err != nil { @@ -163,177 +92,64 @@ func handleNodeRequest(w http.ResponseWriter, r *http.Request) { } defer r.Body.Close() - printDebug("Received HTTP message: %+v", msg) + printDebug("Received message: %+v\n", msg) + w.Write([]byte("Message received")) + interpretMessage(msg) } -func startNodeClientUnix() { - var idCounter uint32 = 0 - +func startNodeClient() { for { - msg := Message{ - ID: idCounter, - Type: MsgTypeTest, - Content: []byte("This is a test message via UNIX socket"), - Target: "node2", ///!!! - } - idCounter++ + for _, peerAddr := range peers { + msg := Message{ + ID: hostID, + Type: "test", + Content: "This is a test message from the client node", + } - if err := sendMessage(msg); err != nil { - printWarn("Send error: %v", err) + err := sendMessage(peerAddr, msg) + if err != nil { + printWarn("Error sending message to %s: %v", peerAddr, err) + } else { + printInfo("Message sent successfully to: %s", peerAddr) + } } time.Sleep(10 * time.Second) } } func interpretMessage(msg Message) { - printDebug("Received message: %s", msg.Content) - switch msg.Type { - case MsgTypeTest: - handleTestMessage(msg) - case MsgTypeSearchTextRequest: + case "test": + printDebug("Received test message: %v", msg.Content) + case "update": + printDebug("Received update message: %v", msg.Content) + go update() + case "heartbeat": + handleHeartbeat(msg.Content) + case "election": + handleElection(msg.Content) + case "search-text": handleSearchTextMessage(msg) - case MsgTypeSearchImageRawRequest, MsgTypeSearchImageThumbRequest, MsgTypeSearchImageFullRequest, MsgTypeSearchImageAllRequest: + case "search-image": handleSearchImageMessage(msg) - case MsgTypeSearchVideoRequest: + case "search-video": handleSearchVideoMessage(msg) - case MsgTypeSearchFileRequest: + case "search-file": handleSearchFileMessage(msg) - case MsgTypeSearchForumRequest: + case "search-forum": handleSearchForumMessage(msg) - case MsgTypeSearchMusicRequest: - handleSearchMusicMessage(msg) - - case MsgTypeSearchTextResponse: - handleTextResultsMessage(msg) - case MsgTypeSearchImageResponse: - handleImageResultsMessage(msg) - case MsgTypeSearchVideoResponse: - handleVideoResultsMessage(msg) - case MsgTypeSearchFileResponse: - handleFileResultsMessage(msg) - case MsgTypeSearchForumResponse: + case "forum-results": handleForumResultsMessage(msg) - case MsgTypeSearchMusicResponse: - handleMusicResultsMessage(msg) - + case "text-results": + handleTextResultsMessage(msg) + case "image-results": + handleImageResultsMessage(msg) + case "video-results": + handleVideoResultsMessage(msg) + case "file-results": + handleFileResultsMessage(msg) default: - printWarn("Unknown message type: %d", msg.Type) - } -} - -// Serialize Message to binary -func serializeMessage(msg Message) ([]byte, error) { - buf := new(bytes.Buffer) - - if err := binary.Write(buf, binary.BigEndian, msg.ID); err != nil { - return nil, err - } - if err := binary.Write(buf, binary.BigEndian, msg.Type); err != nil { - return nil, err - } - - // Content - contentBytes := []byte(msg.Content) - if len(contentBytes) > 65535 { - return nil, errors.New("content too long") - } - if err := binary.Write(buf, binary.BigEndian, uint16(len(contentBytes))); err != nil { - return nil, err - } - buf.Write(contentBytes) - - // Target - targetBytes := []byte(msg.Target) - if len(targetBytes) > 255 { - return nil, errors.New("target name too long") - } - if err := buf.WriteByte(uint8(len(targetBytes))); err != nil { - return nil, err - } - buf.Write(targetBytes) - - return buf.Bytes(), nil -} - -// Deserialize binary to Message -func deserializeMessage(data []byte) (Message, error) { - buf := bytes.NewReader(data) - var msg Message - - if err := binary.Read(buf, binary.BigEndian, &msg.ID); err != nil { - return msg, err - } - if err := binary.Read(buf, binary.BigEndian, &msg.Type); err != nil { - return msg, err - } - - var contentLen uint16 - if err := binary.Read(buf, binary.BigEndian, &contentLen); err != nil { - return msg, err - } - content := make([]byte, contentLen) - if _, err := io.ReadFull(buf, content); err != nil { - return msg, err - } - msg.Content = content - - var targetLen uint8 - if err := binary.Read(buf, binary.BigEndian, &targetLen); err != nil { - return msg, err - } - target := make([]byte, targetLen) - if _, err := io.ReadFull(buf, target); err != nil { - return msg, err - } - msg.Target = string(target) - - return msg, nil -} - -func writeString(buf *bytes.Buffer, s string) error { - if err := binary.Write(buf, binary.BigEndian, uint16(len(s))); err != nil { - return err - } - _, err := buf.Write([]byte(s)) - return err -} - -func readString(buf *bytes.Reader) (string, error) { - var length uint16 - if err := binary.Read(buf, binary.BigEndian, &length); err != nil { - return "", err - } - strBytes := make([]byte, length) - if _, err := io.ReadFull(buf, strBytes); err != nil { - return "", err - } - return string(strBytes), nil -} - -type testPayload struct { - Message string `json:"message"` - ResponseAddr string `json:"ResponseAddr"` -} - -func handleTestMessage(msg Message) { - var payload testPayload - if err := json.Unmarshal([]byte(msg.Content), &payload); err != nil { - printWarn("Failed to parse test payload: %v", err) - return - } - printDebug("Received message: %s", payload.Message) - printInfo("Received TEST message: %s", payload.Message) - - reply := Message{ - ID: msg.ID, - Type: MsgTypeTest, - Content: []byte("hello test"), - Target: payload.ResponseAddr, - } - - if err := sendMessage(reply); err != nil { - printWarn("Failed to send test response: %v", err) + printWarn("Received unknown message type: %v", msg.Type) } } diff --git a/open-search.go b/open-search.go index 40caceb..fefce8f 100644 --- a/open-search.go +++ b/open-search.go @@ -7,60 +7,31 @@ import ( ) type OpenSearchDescription struct { - XMLName xml.Name `xml:"OpenSearchDescription"` - Xmlns string `xml:"xmlns,attr"` - ShortName string `xml:"ShortName"` - LongName string `xml:"LongName"` - Description string `xml:"Description"` - Tags string `xml:"Tags,omitempty"` - InputEncoding string `xml:"InputEncoding"` - OutputEncoding string `xml:"OutputEncoding"` - Images []Image `xml:"Image"` - URLs []URL `xml:"Url"` + XMLName xml.Name `xml:"OpenSearchDescription"` + Xmlns string `xml:"xmlns,attr"` + ShortName string `xml:"ShortName"` + Description string `xml:"Description"` + Tags string `xml:"Tags"` + URLs []URL `xml:"Url"` } type URL struct { Type string `xml:"type,attr"` - Method string `xml:"method,attr,omitempty"` Template string `xml:"template,attr"` } -type Image struct { - Height int `xml:"height,attr"` - Width int `xml:"width,attr"` - Type string `xml:"type,attr"` - URL string `xml:",chardata"` -} - func generateOpenSearchXML(config Config) { - baseURL := addProtocol(config.Domain) + // Ensure that language is initialized in `main` before calling this function + baseURL := addProtocol(config.Domain) opensearch := OpenSearchDescription{ - Xmlns: "http://a9.com/-/spec/opensearch/1.1/", - ShortName: Translate("site_name"), - LongName: Translate("site_name") + " Search", - Description: Translate("site_description"), - Tags: Translate("site_tags"), - InputEncoding: "UTF-8", - OutputEncoding: "UTF-8", - Images: []Image{ - { - Height: 512, - Width: 512, - Type: "image/svg+xml", - URL: fmt.Sprintf("%s/static/images/icon-512.svg", baseURL), - }, - { - Height: 16, - Width: 16, - Type: "image/png", - URL: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAABYAAAAWAF42ktiAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAA2pJREFUOI1lk11oW3UYxp//ycnhpHqapmlssrZLkzRbu9qPrVYWxhTpsLPD4fRiDBmj6ubwZhfChCHSgjBB59B5McEboepWVkSLdQ432rVZ2s3B0jQtga7LcnKa9CNtlrT56EnO64XbqPrCe/c8P3hfnocRETZPMpm0hyPK52VGaa8gCCUMTMtkMwsg9CWTK1+2tbVlNuvZJgAfnA5d2eZ2HvQHguza9WHcvx+GXs+jtaUJBw90UonBsL6ykjjhdDp/ekogIhCR7sGDh1MLi0v01pFuKjHX0rPPOana3UqiqYYM5VvJaHPT+a8vkqqqxVAodPyx7x9AIDjTt5xYoVZPB4mmGhLLqqn5hZfo+o0R+uhMLz1TYadDh4/R4aMn6OPes5TP5wt+v383EYFlMpkqnuflD06dZn2XBmAyGZFYTuDke8cQmAqiproKjTsacPG77yEaRIQjCoZ+/oHcLoe/asuWndxcOPxhVJlnlwd+Ba/jsLt9F3gdh6bGevjGb0OZn4f31jgURcHcXBikaTj7xQVms1pbxsbGtnGCIHZ5fbehFgpQVRW/Df2BN15/DaNeH9SNHNwuB4auXntyLwDCqHcC2VyOLBZLB8frOCkiK2CPn9q1fx9cDjsWFuIg0rDdXQcdx2CzVYJAYGAACPH4kiYIQhUHsKLRWAqDKAIA9r/agc/OfQWtSPi09xOAAUVNQywWBwDs2tmMlqZGGI0SCoVCikulUtONDduRzeUAxqCqKgCGib/uQpJKsaEWwBiHfa+8DMY4+APTiMUXYS436dLp9AxXYbZ8s8fzIpwOO2zWSmSzeeyor0dLczNGb40DYGCcDge6OsEYQ7FYxPvvHkUqlUqPjIz8yYgI0WgsNhcOW/suDWDM68Px7rfBcTqo6gaICJJUCq9vAld+GUKdy4E7N69qsiyfc7vdp0FEmJ2dfX5tfb3w4+UBkipdJJpqSLLUkslWR2WVLjKUb6U3j3RT+95OisjRoizLgf7+fuFpEokIwVCoYzWZzAVnQvTOyVNUaq0jg9lOBrOdahva6fyFbym9tqY9jMiBwcHBin9F+clOTk6a7vknf08+epTP5zdImY/R0nJC0zSNolFl0R8InOnp6eE3e9h/6wwAw8PDvNVq3aPX6x2qquZXV1enPB7PFID/if8GRa7Q/nLxcNoAAAAASUVORK5CYII=", - }, - }, + Xmlns: "http://a9.com/-/spec/opensearch/1.1/", + ShortName: Translate("site_name"), + Description: Translate("site_description"), + Tags: Translate("site_tags"), URLs: []URL{ { Type: "text/html", - Method: "get", Template: fmt.Sprintf("%s/search?q={searchTerms}", baseURL), }, { @@ -77,8 +48,6 @@ func generateOpenSearchXML(config Config) { } defer file.Close() - file.WriteString(`` + "\n") - enc := xml.NewEncoder(file) enc.Indent(" ", " ") if err := enc.Encode(opensearch); err != nil { 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/black.css b/static/css/black.css index 1738ec1..9661246 100644 --- a/static/css/black.css +++ b/static/css/black.css @@ -30,7 +30,7 @@ --green: #31b06e; --search-button: #BABCBE; - + --image-view: #161616; --image-view-titlebar: #161616; --view-image-color: #000000; @@ -38,7 +38,7 @@ --fff: #fff; --publish-info: #7f869e; - + color-scheme: dark; } @@ -71,4 +71,4 @@ .view-image-search:hover { box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); -} \ No newline at end of file +} diff --git a/static/css/dark.css b/static/css/dark.css index 3af919c..0555cd3 100644 --- a/static/css/dark.css +++ b/static/css/dark.css @@ -30,7 +30,7 @@ --green: #31b06e; --search-button: #BABCBE; - + --image-view: #161616; --image-view-titlebar: #161616; --view-image-color: #000000; @@ -38,7 +38,7 @@ --fff: #fff; --publish-info: #7f869e; - + color-scheme: dark; } @@ -71,4 +71,4 @@ .view-image-search:hover { box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); -} \ No newline at end of file +} diff --git a/static/css/latte.css b/static/css/latte.css index 6d28327..58eee36 100644 --- a/static/css/latte.css +++ b/static/css/latte.css @@ -25,7 +25,7 @@ --base: #1e1e2e; --mantle: #181825; --crust: #11111b; - + --html-bg: var(--base); --font-fg: var(--text); --fg: var(--subtext0); @@ -53,7 +53,7 @@ --highlight: var(--subtext1); --search-button: var(--subtext0); - + --image-view: var(--mantle); --image-view-titlebar: var(--mantle); --view-image-color: var(--crust); @@ -61,7 +61,7 @@ --fff: var(--text); --publish-info: var(--overlay2); - + color-scheme: dark; } @@ -94,4 +94,4 @@ .view-image-search:hover { box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); -} \ No newline at end of file +} diff --git a/static/css/light.css b/static/css/light.css index 30677fd..656f89a 100644 --- a/static/css/light.css +++ b/static/css/light.css @@ -28,7 +28,7 @@ --blue: #4285f4; --green: #202124; - + --image-view: #ffffff; --image-view-titlebar: #ffffff; --view-image-color: #f1f3f4; @@ -69,4 +69,4 @@ .view-image-search:hover { box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important; transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important; -} \ No newline at end of file +} diff --git a/static/css/mocha.css b/static/css/mocha.css index 3b7c869..543332f 100644 --- a/static/css/mocha.css +++ b/static/css/mocha.css @@ -25,7 +25,7 @@ --base: #eff1f5; --mantle: #e6e9ef; --crust: #dce0e8; - + --html-bg: var(--base); --font-fg: var(--text); --fg: var(--subtext0); @@ -56,7 +56,7 @@ --green: var(--green); --search-button: var(--subtext0); - + --image-view: var(--mantle); --image-view-titlebar: var(--mantle); --view-image-color: var(--crust); @@ -64,7 +64,7 @@ --fff: var(--text); --publish-info: var(--overlay2); - + color-scheme: light; } @@ -97,4 +97,4 @@ .view-image-search:hover { box-shadow: 0 14px 28px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.12); -} \ No newline at end of file +} diff --git a/static/css/night.css b/static/css/night.css index 41e910c..0676672 100644 --- a/static/css/night.css +++ b/static/css/night.css @@ -28,7 +28,7 @@ --blue: #8ab4f8; --green: #31b06e; - + --image-view: #0c0d0f; --image-view-titlebar: #0c0d0f; --view-image-color: #000000; @@ -38,7 +38,7 @@ --search-button: #BABCBE; --publish-info: #7f869e; - + color-scheme: dark; } @@ -71,4 +71,4 @@ .view-image-search:hover { box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22); -} \ No newline at end of file +} diff --git a/static/css/style-fixedwidth.css b/static/css/style-fixedwidth.css index 64632eb..ce88cc4 100644 --- a/static/css/style-fixedwidth.css +++ b/static/css/style-fixedwidth.css @@ -1,10 +1,7 @@ /* Ensure the body width is fixed and prevents resizing by the user */ body { - overflow-x: hidden; - /* Prevent horizontal scrolling by user */ - width: 100vw; - /* Fix the width of the viewport */ - max-width: 100vw; - /* Prevent page from extending wider than the viewport */ + overflow-x: hidden; /* Prevent horizontal scrolling by user */ + width: 100vw; /* Fix the width of the viewport */ + max-width: 100vw; /* Prevent page from extending wider than the viewport */ box-sizing: border-box; } \ No newline at end of file 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 79cc585..0000000 --- a/static/css/style-imageloading.css +++ /dev/null @@ -1,65 +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 */ - } -} \ No newline at end of file diff --git a/static/css/style-imageviewer.css b/static/css/style-imageviewer.css index f9e44ed..ac6874a 100644 --- a/static/css/style-imageviewer.css +++ b/static/css/style-imageviewer.css @@ -1,15 +1,12 @@ /* Image Viewer Overlay */ #image-viewer-overlay { position: fixed; - top: 105px; + top: 105px; right: 0; - width: 33%; - /* Occupies the right space for the viewer */ - height: calc(100% - 105px); - /* Adjust height */ + width: 33%; /* Occupies the right space for the viewer */ + height: calc(100% - 105px); /* Adjust height */ z-index: 999; - display: none; - /* Initially hidden */ + display: none; /* Initially hidden */ /* Added transition for opening animation */ /* transform: translateX(100%); @@ -21,10 +18,8 @@ position: fixed; top: 105px; right: 0; - width: 33%; - /* Match the width of the overlay */ - height: calc(100% - 105px); - /* Adjust height */ + width: 33%; /* Match the width of the overlay */ + height: calc(100% - 105px); /* Adjust height */ background: var(--search-bg); padding: 20px; box-sizing: border-box; @@ -38,7 +33,6 @@ #viewer-image { max-width: 100%; max-height: 60vh; - border-radius: 5px; } /* Viewer Title */ @@ -63,8 +57,7 @@ display: flex; flex-direction: row; align-items: center; - gap: 5px; - /* Add spacing between buttons */ + gap: 5px; /* Add spacing between buttons */ } #viewer-close-button, @@ -92,8 +85,7 @@ /* Adjust the images container when the viewer is visible */ .images { - margin-right: 33%; - /* Reserve space for the image viewer */ + margin-right: 33%; /* Reserve space for the image viewer */ } /* Clickable Elements */ @@ -101,15 +93,6 @@ cursor: pointer; } - - -.search-type-icons { - display: flex; - justify-content: center; - flex-wrap: wrap; - gap: 32px; -} - /* Viewer Image Link */ #viewer-image-link { text-decoration: none; @@ -118,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 */ @@ -170,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; - } - - .search-type-icons { - gap: 16px; - } - - #viewer-image-container { - margin-top: 5px; } #viewer-image { @@ -195,7 +168,6 @@ } .images { - margin-right: 0; - /* No reserved space on smaller screens */ + margin-right: 0; /* No reserved space on smaller screens */ } -} \ No newline at end of file +} diff --git a/static/css/style-instantanswer.css b/static/css/style-instantanswer.css deleted file mode 100644 index 77bd088..0000000 --- a/static/css/style-instantanswer.css +++ /dev/null @@ -1,310 +0,0 @@ -*, *::before, *::after { - box-sizing: border-box; -} - -.instant-container { - position: absolute; - top: 140px; - right: 175px; - width: 500px; - z-index: 1; -} - -.instant-box { - border: 1px solid var(--snip-border); - background-color: var(--snip-background); - border-radius: 8px; - padding: 15px; - margin-bottom: 20px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); -} - -.instant-box h3 { - margin-top: 0; - color: var(--highlight); - font-size: 20px; - border-bottom: 1px solid var(--border); - padding-bottom: 10px; - text-decoration: none; -} - -.instant-result { - margin: 15px 0; - font-size: 16px; - line-height: 1.5; -} - -.calc-container { - display: flex; - flex-direction: column; - gap: 10px; - margin-top: 15px; -} - -.calc-input { - box-sizing: border-box; - width: 100%; - padding: 10px; - font-size: 18px; - border: 1px solid var(--border); - border-radius: 4px; - background-color: var(--search-bg); - color: var(--fg); -} - -.calc-buttons { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 5px; -} - -.calc-buttons button { - padding: 10px; - font-size: 18px; - border: 1px solid var(--border); - border-radius: 4px; - background-color: var(--button); - color: var(--fg); - cursor: pointer; - transition: background-color 0.2s; -} - -.calc-buttons button:hover { - background-color: var(--search-select); -} - -.calc-result { - margin-top: 10px; - padding: 10px; - font-size: 20px; - border: 1px solid var(--border); - border-radius: 4px; - background-color: var(--snip-background); -} - -.calc-history { - display: flex; - flex-direction: column-reverse; - font-family: monospace; - font-size: 16px; - color: var(--fg); - background-color: var(--search-bg); - border: 1px solid var(--border); - padding: 10px; - border-radius: 4px; - height: 120px; - overflow-y: auto; - white-space: pre-line; - overflow-anchor: none; -} - -.calc-buttons { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; -} - -.calc-buttons button { - padding: 20px; - font-size: 20px; - border-radius: 6px; - background-color: var(--search-bg); - border: 1px solid var(--border); - color: var(--fg); -} - -.calc-buttons .equals { - background-color: #2ecc71; - color: #fff; - font-weight: bold; -} - -.calc-buttons .equals:hover { - background-color: #27ae60; -} - -.weather { - padding: 0; - background: none; - border-radius: 0; - box-shadow: none; - color: var(--snip-text); - min-width: 0; - max-width: none; - margin: 0; -} - -.weather-header { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 1.13em; - font-weight: 500; - margin-bottom: 6px; -} - -.weather-location { - opacity: 0.82; -} - -.weather-temp { - font-size: 2.3em; - font-weight: 700; - color: var(--blue); /* not sure if using "var(--blue);" is the right idea */ - letter-spacing: -1px; -} - -.weather-deg { - font-size: 0.5em; - font-weight: 500; - vertical-align: super; - opacity: 0.5; -} - -.weather-current { - display: flex; - align-items: center; - margin-bottom: 12px; - gap: 10px; - padding: 12px 14px 10px 14px; -} - -.weather-icon { - font-size: 1.7em; - margin-right: 7px; -} - -.weather-cond { - font-size: 1.09em; - font-weight: 500; - color: var(--highlight); - margin-right: 12px; - display: flex; - align-items: center; - gap: 5px; -} - -.weather-details { - font-size: 0.97em; - opacity: 0.82; - margin-left: auto; -} - -.weather-details span { - color: var(--fg); -} - -.weather-forecast { - display: flex; - gap: 7px; - margin-top: 9px; -} - -.weather-forecast-day { - flex: 1 1 0; - min-width: 0; - background: var(--search-bg); - border-radius: 8px; - padding: 8px 6px; - text-align: center; - color: var(--font-fg); - box-shadow: 0 1px 3px color-mix(in srgb, var(--search-bg), black 15%); -} - -.weather-forecast-date { - font-size: 0.97em; - font-weight: 500; - opacity: 0.82; - margin-bottom: 2px; - color: var(--highlight); -} - -.weather-forecast-temps { - font-size: 1.13em; - font-weight: 600; - color: var(--blue); -} - -.weather-forecast-cond { - font-size: 0.98em; - opacity: 0.83; - color: var(--fg); -} - - -.weather-current { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; - gap: 8px; -} - -.weather-current-left { - display: flex; - align-items: center; - gap: 7px; -} - -.weather-icon { - font-size: 1.7em; -} - -.weather-cond { - font-size: 1.08em; - font-weight: 500; - color: var(--highlight); -} - -.weather-current-right { - display: flex; - align-items: center; - gap: 12px; -} - -.weather-detail { - font-size: 0.98em; - color: var(--blue); - opacity: 0.85; - font-weight: 500; -} - -/* Responsive adjustments */ -@media only screen and (max-width: 1450px) { - .instant-container { - right: 75px; - } -} - -@media only screen and (max-width: 1350px) { - .instant-container { - right: 5px; - } -} - -@media only screen and (max-width: 1290px) { - .instant-container { - position: relative; - top: 0; - right: 0; - margin-left: 175px; - margin-top: 20px; - } -} - -@media only screen and (max-width: 880px) { - .instant-container { - width: 90%; - margin-left: 20px; - } -} - -@media (max-width: 540px) { - .weather-forecast { - gap: 4px; - } - .weather-forecast-day { - padding: 6px 2px; - font-size: 0.95em; - } -} diff --git a/static/css/style-loadingcircle.css b/static/css/style-loadingcircle.css deleted file mode 100644 index ce65c0b..0000000 --- a/static/css/style-loadingcircle.css +++ /dev/null @@ -1,32 +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); - } -} \ No newline at end of file diff --git a/static/css/style-loadingindicator.css b/static/css/style-loadingindicator.css deleted file mode 100644 index 5068c0c..0000000 --- a/static/css/style-loadingindicator.css +++ /dev/null @@ -1,69 +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; -} \ No newline at end of file diff --git a/static/css/style-map.css b/static/css/style-map.css deleted file mode 100644 index afdc8af..0000000 --- a/static/css/style-map.css +++ /dev/null @@ -1,134 +0,0 @@ -/* Unable to find ... message */ -.message { - position: absolute; - bottom: 20px; - left: 50%; - transform: translateX(-50%); - padding: 10px; - background-color: var(--search-bg); - border: 1px solid var(--border); - border-radius: 5px; - box-shadow: 0 0 10px var(--box-shadow); - z-index: 1000; - width: auto; - max-width: 80%; - text-align: center; - color: var(--text-color); -} - -/* Map container */ -#map { - height: calc(100% - 65px); - width: 100%; - top: 65px; -} - -.leaflet-control-layers-toggle { - background-color: var(--button) !important; - border: 1px solid var(--border) !important; - color: var(--fg) !important; -} - -.leaflet-bar a, -.leaflet-bar a:hover { - background-color: var(--button) !important; - border: 1px solid var(--border) !important; - color: var(--fg) !important; -} - -.leaflet-popup-content-wrapper, -.leaflet-popup-tip { - background: var(--html-bg) !important; - color: var(--text-color) !important; - border: 1px solid var(--border) !important; -} - -.leaflet-popup-content-wrapper a { - color: var(--link) !important; -} - -.leaflet-popup-content-wrapper a:hover { - text-decoration: underline; -} - -/* Leaflet control buttons */ -.leaflet-control-locate, -.leaflet-control-layers-toggle { - background-color: var(--button) !important; - border: 1px solid var(--border) !important; - color: var(--fg) !important; -} - -.leaflet-bar a, -.leaflet-bar a:focus, -.leaflet-bar a:active, -.leaflet-bar a:hover { - background-color: var(--button) !important; - border: 1px solid var(--border) !important; - color: var(--fg) !important; - box-shadow: 0 2px 12px rgba(0,0,0,0.24) !important; - transition: background-color 0.2s; -} - -.leaflet-control-layers-toggle, -.leaflet-control-layers-expanded { - background-color: var(--button) !important; - border: 1px solid var(--border) !important; - color: var(--fg) !important; -} - -.leaflet-bar a:hover, -.leaflet-bar a:focus { - background-color: var(--search-select) !important; -} - -.leaflet-popup-content-wrapper, -.leaflet-popup-tip { - background: var(--html-bg) !important; - color: var(--text-color) !important; - border: 1px solid var(--border) !important; -} - -.leaflet-popup-content-wrapper a { - color: var(--link) !important; -} - -.leaflet-popup-content-wrapper a:hover { - text-decoration: underline; -} - -/* Leaflet attribution control */ -.leaflet-control-attribution { - background: var(--button) !important; - border: 1px solid var(--border) !important; - color: var(--fg) !important; -} - -.leaflet-control-attribution a { - color: var(--link) !important; -} - -.leaflet-control-attribution a:hover { - text-decoration: underline !important; -} - -/* Leaflet scale control */ -.leaflet-control-scale { - background: var(--button) !important; - border: 1px solid var(--border) !important; - color: var(--fg) !important; - text-shadow: 1px 1px 2px var(--border) !important; -} - -.leaflet-control-scale-line { - background: var(--button) !important; - border: 1px solid var(--border) !important; - color: var(--fg) !important; - text-shadow: 1px 1px 2px var(--border) !important; -} - -@media only screen and (max-width: 880px) { - #map { - top: 130px !important; - } -} \ No newline at end of file diff --git a/static/css/style-menu.css b/static/css/style-menu.css index c311764..95be6cf 100644 --- a/static/css/style-menu.css +++ b/static/css/style-menu.css @@ -165,45 +165,37 @@ /* Spacing & sizing */ padding: 32px; - max-width: 600px; - /* Increased width */ - max-height: 80vh; - /* Optional: restrict height to 80% of viewport */ - overflow-y: auto; - /* Enable scrolling if content exceeds height */ + max-width: 600px; /* Increased width */ + max-height: 80vh; /* Optional: restrict height to 80% of viewport */ + overflow-y: auto; /* Enable scrolling if content exceeds height */ color: var(--font-fg); } #aboutQGatoModal #close-button { position: absolute; top: 12px; - right: 12px; - /* Moved close button to top-right */ + right: 12px; /* Moved close button to top-right */ } #aboutQGatoModal .modal-content { text-align: center; - margin-top: 20px; - /* Adjusted spacing */ + margin-top: 20px; /* Adjusted spacing */ } /* Logo */ #aboutQGatoModal .modal-content img { - width: 100px; - /* Increased logo size */ + width: 100px; /* Increased logo size */ margin-bottom: 16px; } /* Headings, paragraphs, etc. */ #aboutQGatoModal .modal-content h2 { - font-size: 2rem; - /* Larger heading */ + font-size: 2rem; /* Larger heading */ margin: 8px 0; } #aboutQGatoModal .modal-content p { - font-size: 1.1rem; - /* Larger paragraph text */ + font-size: 1.1rem; /* Larger paragraph text */ margin: 12px 0; } @@ -221,10 +213,8 @@ color: var(--font-fg); border: 1px solid var(--border); border-radius: 6px; - padding: 12px 16px; - /* Larger button padding */ - font-size: 1rem; - /* Larger button text */ + padding: 12px 16px; /* Larger button padding */ + font-size: 1rem; /* Larger button text */ cursor: pointer; transition: border 0.3s ease, background-color 0.3s ease, color 0.3s ease; } @@ -235,8 +225,7 @@ /* Close Button Style */ .cloase-btn { - font-size: 1.5rem; - /* Larger close button */ + font-size: 1.5rem; /* Larger close button */ color: var(--search-button); border-radius: 50%; padding: 8px; diff --git a/static/css/style-music.css b/static/css/style-music.css deleted file mode 100644 index 01ed5cc..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-privacy.css b/static/css/style-privacy.css index 9037102..5cfef4b 100644 --- a/static/css/style-privacy.css +++ b/static/css/style-privacy.css @@ -92,4 +92,4 @@ .privacy-section .cookie-table { margin-left: auto; margin-right: auto; -} \ No newline at end of file +} diff --git a/static/css/style-search.css b/static/css/style-search.css index 9505517..446f3d2 100644 --- a/static/css/style-search.css +++ b/static/css/style-search.css @@ -1,6 +1,5 @@ /* General Styles */ -body, -html { +body, html { margin: 0; padding: 0; background-color: var(--html-bg); @@ -9,8 +8,7 @@ html { color: var(--text-color); } -button, -p { +button, p { font-family: 'Inter', Arial, Helvetica, sans-serif; font-weight: 400; } @@ -103,15 +101,13 @@ body.menu-open { text-align: left; width: 100%; position: absolute; - left: -1px; - /* this is so curesed */ + left: -1px; /* this is so curesed */ top: 100%; background-color: var(--search-bg-input); border-bottom-left-radius: 22px; border-bottom-right-radius: 22px; border: 1px solid var(--search-bg-input-border); - border-top: none; - /* No top border to avoid double borders with the input */ + border-top: none; /* No top border to avoid double borders with the input */ z-index: 900; } @@ -163,10 +159,8 @@ body.menu-open { text-align: center; display: flex; flex-direction: column; - justify-content: center; - /* Center vertically */ - height: 100vh; - /* Make it full viewport height */ + justify-content: center; /* Center vertically */ + height: 100vh; /* Make it full viewport height */ } /* Search Type Icons */ @@ -177,8 +171,7 @@ body.menu-open { margin-top: 30px; gap: 20px; /* Allow transform movement */ - transition: transform 0.3s ease; - /* Smooth movement when suggestions appear */ + transition: transform 0.3s ease; /* Smooth movement when suggestions appear */ position: relative; } @@ -249,8 +242,7 @@ body.menu-open { z-index: 999; width: 50px; height: 50px; - font-size: 36px; - /* Not working */ + font-size: 36px; /* Not working */ } .side-nav .closebtn:hover { @@ -267,13 +259,11 @@ body.menu-open { right: 0; background-color: var(--html-bg); overflow-x: hidden; - transition: width 0.4s ease-in-out; - /* Does this even change anything? cant really tell */ + transition: width 0.4s ease-in-out; /* Does this even change anything? cant really tell */ z-index: 1000; - box-shadow: -2px 0 5px rgba(0, 0, 0, 0.5); + box-shadow: -2px 0 5px rgba(0,0,0,0.5); padding-top: 70px; - overflow-x: hidden; - /* Prevents horizontal overflow */ + overflow-x: hidden; /* Prevents horizontal overflow */ } .side-nav:hover .closebtn:hover { @@ -332,7 +322,7 @@ body.menu-open::before { left: 0; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.5); + background: rgba(0,0,0,0.5); z-index: 998; } @@ -347,13 +337,11 @@ body.menu-open::before { .wrapper { width: 90%; } - .logo-container svg { width: 75%; max-width: 90%; min-width: 25%; } - .side-nav { max-width: 90%; } @@ -390,8 +378,7 @@ p { line-height: 1.58; } -input, -button { +input, button { outline: none; } @@ -400,14 +387,11 @@ body.menu-open { } /* Prevent horizontal scrolling on the page */ -html, -body { - overflow-x: hidden; - /* Disables horizontal scrollbar */ +html, body { + overflow-x: hidden; /* Disables horizontal scrollbar */ margin: 0; padding: 0; - max-width: 100vw; - /* Limits content to viewport width */ + max-width: 100vw; /* Limits content to viewport width */ } /* @@ -416,4 +400,4 @@ body { outline: none; border: 1px solid var(--font-fg); box-shadow: none; -} */ \ No newline at end of file +} */ diff --git a/static/css/style-settings.css b/static/css/style-settings.css index 73e73e8..6719fa7 100644 --- a/static/css/style-settings.css +++ b/static/css/style-settings.css @@ -7,7 +7,6 @@ object-position: center; vertical-align: bottom; border-radius: 4px; - position: relative; } .theme-link { @@ -17,8 +16,7 @@ width: 48%; margin-bottom: 10px; height: 150px; - position: relative; - /* Make it possible to position the tooltip */ + position: relative; /* Make it possible to position the tooltip */ } .theme-link img { @@ -35,34 +33,21 @@ } */ .theme-tooltip { + display: none; /* Hidden by default */ position: absolute; + bottom: 10px; /* Position at the bottom of the image */ left: 50%; - bottom: 14px; - transform: translateX(-50%) translateY(100%); - background: var(--search-bg); - color: var(--font-fg); + transform: translateX(-50%); + background-color: rgba(0, 0, 0, 0.7); /* Semi-transparent background */ + color: #fff; + padding: 5px 10px; + border-radius: 4px; font-size: 14px; - padding: 7px 14px; - border-radius: 6px; - box-shadow: 0 5px 24px 0 rgba(0,0,0,0.25); - opacity: 0; - pointer-events: none; - z-index: 5; white-space: nowrap; - transition: opacity 0.22s cubic-bezier(.7, .1, .6, 1), transform 0.22s cubic-bezier(.7, .1, .6, 1); } -.theme-link:hover .theme-tooltip, -.theme-link:focus .theme-tooltip { - opacity: 1; - transform: translateX(-50%) translateY(0); - pointer-events: auto; -} - -.theme-link.show-tooltip .theme-tooltip { - opacity: 1; - transform: translateX(-50%) translateY(0); - pointer-events: auto; +.theme-link:hover .theme-tooltip { + display: block; /* Show tooltip on hover */ } .themes-settings-menu { @@ -77,7 +62,7 @@ } #searchLanguageSelect, -#safeSearchSelect, +#safeSearchSelect, #siteLanguageSelect { border-radius: 4px; padding: 6px; @@ -90,12 +75,11 @@ cursor: pointer; transition: all 0.3s ease; text-align: center; - box-sizing: border-box; - /* Ensures consistent width with padding */ + box-sizing: border-box; /* Ensures consistent width with padding */ } #searchLanguageSelect:hover, -#safeSearchSelect:hover, +#safeSearchSelect:hover, #siteLanguageSelect:hover { border: 1px solid #5f6368; /* background-color: var(--button-hover); */ @@ -104,7 +88,7 @@ .save.save-settings-page { padding: 6px; width: 160px; - height: 40px; + height: 40px; } /* Ensure correct alignment */ @@ -114,9 +98,9 @@ justify-content: space-between; } -.settings-row select, +.settings-row select, .settings-row button { width: 160px; height: 40px; box-sizing: border-box; -} \ No newline at end of file +} diff --git a/static/css/style.css b/static/css/style.css index 9561c86..e4b1cd6 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -16,7 +16,7 @@ white-space: nowrap; word-wrap: normal; direction: ltr; - + /* Support for all WebKit browsers. */ -webkit-font-feature-settings: 'liga'; -webkit-font-smoothing: antialiased; @@ -28,7 +28,8 @@ font-feature-settings: 'liga'; } -#search-wrapper-ico { +#search-wrapper-ico, +#clearSearch { background: none; border: none; color: var(--fg); @@ -38,7 +39,8 @@ cursor: pointer; } -#search-wrapper-ico:hover { +#search-wrapper-ico:hover, +#clearSearch:hover { transition: all .3s ease; color: var(--font-fg); } @@ -53,17 +55,18 @@ margin-right: 0px; } -.fetched_dif_videos { +#clearSearch { + right: 40px; + visibility: hidden; +} + +.fetched_dif { margin-top: 110px !important; } -.fetched_dif_files { - margin-top: 10px !important; -} - - -.fetched_dif_images { - margin-top: 10px; +.fetched_img { + margin-left: 1.2% !important; + left: 0px !important; } .clean { @@ -107,68 +110,122 @@ html { font-size: 16px; } -/* container */ +#info_font { + font-size: 15px; +} +.calc { + height: fit-content; + width: fit-content; + position: relative; + left: 175px; + border: 1px solid var(--snip-border); + background-color: var(--snip-background); + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; +} + +.calc-btn-style { + background-color: var(--html-bg) !important; +} + +.calc-input { + width: 90%; + height: 10%; + background-color: var(--search-bg); + border: 1px solid var(--snip-border); + border-radius: 8px; + padding: 15px; + margin-top: 8px; + text-align: right; + max-width: 48em; + line-height: 1.58; + font-size: 22px; + color: var(--fg); + letter-spacing: normal; + overflow: hidden; +} + +.calc-btn { + max-width: 48em; + line-height: 50px; + font-size: 22px; + color: var(--fg); + letter-spacing: normal; + border-radius: 8px; + background-color: var(--search-bg); + border: 1px solid var(--snip-border); + height: 50px; + margin: auto; + margin: 4px; + width: 80px; + text-align: center; +} + +.calc-btn-2 { + max-width: 48em; + line-height: 50px; + font-size: 22px; + color: var(--fff); + letter-spacing: normal; + border-radius: 8px; + background-color: var(--font-fg); + height: 50px; + margin: auto; + margin: 4px; + width: 80px; + text-align: center; +} + +.calc-btns { + display: grid; + grid-template-columns: repeat(4, 90px); + width: 100%; + justify-content: center; + padding: 4px; +} + +.calc-pos-absolute { + position: absolute; + margin-top: 60px; + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.prev_calculation { + opacity: 0.5; + font-size: 14px; +} + +.emoji-code { + font-family: Arial, Helvetica, sans-serif; +} + +/* container */ .images { - margin: 0; display: flex; flex-wrap: wrap; - margin-right: 35.4%; - position: relative; + justify-content: center; } .image { - flex-grow: 1; - padding: .5rem .5rem 3rem .5rem; + flex: 1 1 auto; + padding: .5rem .5rem 2rem .5rem; margin: .25rem; border-radius: 12px; height: 10rem; + max-width: 20rem; + min-width: 3rem; transition: outline 0.3s ease, transform 0.3s ease; } -.image>a { - position: relative; - text-decoration: none; -} - -.image img { - transition: .3s; -} - -.resolution { - transition: .3s !important; -} - -.image:hover img { - box-shadow: 0 14px 28px var(--view-image-color); -} - -.img_title { - display: block; - position: absolute; - width: 100%; - font-size: .9rem; - color: var(--fg); - padding: .5rem 0 0 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.img_title { - color: var(--blue); -} - -.image img { - margin: 0; - padding: 0; - height: 100%; - width: 100%; - object-fit: cover; - vertical-align: bottom; - border-radius: 12px; - box-sizing: border-box; - border: 1px solid var(--border); +.image_selected { + outline: 1px solid var(--border); + transform: scale(1.05); } .image>a { @@ -237,13 +294,64 @@ html { } .btn-nostyle { - font-family: 'Inter', Arial, Helvetica, sans-serif !important; background-color: inherit; border: none; padding: 0px; width: fit-content; } +.view-image { + background-color: var(--view-image-color); + width: 100%; + height: auto; + display: flex; + justify-content: center; + align-items: center; +} + +.view-image img { + max-width: 100%; + max-height: 427px; +} + +.image_view a { + text-decoration: none; + color: var(--link); + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.image_view a:hover { + text-decoration: underline; +} + +.image_view p { + margin-left: 20px; + margin-right: 20px; + display: block; +} + +.image_view a { + display: block; +} + +hr { + border: 0; + border-top: 1px solid var(--border); + margin: 0; + padding: 0; +} + +.image_hide { + display: none !important; +} + +.image_show { + display: block !important; +} + .icon_visibility { visibility: visible; } @@ -267,11 +375,9 @@ html { 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); @@ -291,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 { @@ -322,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 { @@ -384,7 +483,11 @@ this is so stupid, separate css into general style and per result page css style } #theme, -#safe { +#safe, +#open-new-tab, +#domain, +#javascript-setting, +#ux_lang { border-radius: 4px; padding: 6px; font-size: 15px; @@ -397,11 +500,26 @@ this is so stupid, separate css into general style and per result page css style } #theme:hover, -#safe:hover { +#safe:hover, +#open-new-tab:hover, +#domain:hover, +#javascript-setting:hover, +#ux_lang:hover { border: 1px solid #5f6368; cursor: pointer; } +.domain { + margin-top: 10px; + border: 1px solid var(--button); + border-radius: 4px; + padding: 6px; + font-size: 15px; + color: var(--font-fg); + width: 90%; + background: var(--button); +} + .save { border-radius: 4px; padding: 6px; @@ -419,6 +537,10 @@ this is so stupid, separate css into general style and per result page css style cursor: pointer; } +.settings-row2 { + border-bottom: 1px solid var(--html-bg) !important; +} + .font-hide { color: var(--html-bg); } @@ -445,7 +567,7 @@ this is so stupid, separate css into general style and per result page css style .settings-row { display: flex; align-items: center; - justify-content: flex-end; + justify-content: flex-end; padding: 2px; padding-left: 10px; padding-right: 10px; @@ -459,6 +581,19 @@ this is so stupid, separate css into general style and per result page css style margin: 0; } +.kno_wiki { + display: none; +} + +.kno_wiki_show { + display: initial !important; + border-radius: 6px; +} + +.open-in-new-tab * { + display: inline-block; +} + .results_settings { color: var(--fg); left: 175px; @@ -488,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 { @@ -516,12 +647,24 @@ form.torrent-sort { transition: all .3s ease; } +.settings-results-link { + position: absolute; + color: var(--fg); + text-decoration: none; +} +.settings-results-link, .settings-icon-link { color: var(--fg); text-decoration: none; } +.settings-results-link { + margin-left: 24px; + display: block; + margin-top: 1px; +} + .wrapper-results input { padding: 10px; } @@ -583,6 +726,10 @@ button { font-family: 'Inter'; } +.X { + color: var(--font-fg); +} + .search-container input { width: 90%; color: var(--font-fg); @@ -610,6 +757,42 @@ p { font-size: 14px; } +.check { + display: flex; + position: relative; + left: 175px; + width: 600px; + word-wrap: break-word; + font-size: 15px; + max-width: 48em; + line-height: 1.58; +} + +.check p, +.check h3 { + font-size: 16px; + margin-bottom: 2px; + margin-top: 2px; +} + +.check p { + color: #ff7769; +} + +.check a { + color: var(--link); + text-decoration: none; +} + +.check h3 { + margin-left: 4px; + font-style: italic; +} + +.check a:hover { + text-decoration: underline; +} + .torrent h3 { font-size: 16px !important; } @@ -658,6 +841,10 @@ p { border-top: 1px solid var(--border); } +#search_footer { + position: fixed; +} + .footer a, .footer a:hover, .footer a:visited { @@ -695,7 +882,36 @@ p { max-width: 600px; word-wrap: break-word; margin-bottom: 35px; - z-index: 0; +} + +.result_sublink { + position: relative; + left: 175px; + margin-top: 10px; + max-width: 600px; + word-wrap: break-word; +} + +.sublinks { + position: relative; + left: 14px; + max-width: 600px; + word-wrap: break-word; +} + +.search-button-wrapper button:hover { + border: 1px solid #5f6368; + cursor: pointer; +} + +.search-button-wrapper button { + color: var(--font-fg); + background-color: var(--button); + font-size: 14px; + border: 1px solid var(--border); + border-radius: 4px; + padding: 13px 10px 13px 10px; + margin: 70px 60px 0px 60px; } .no-decoration { @@ -709,8 +925,8 @@ p { text-decoration: none; } -.js-enabled { - display: none; +.js-enabled { + display: none; } .logomobile { @@ -765,8 +981,7 @@ p { :root { /*--search-bg-input: #1a1a1a; /* Example background color */ - --search-bg-input-border: #2a2a2a; - /* Visible border color */ + --search-bg-input-border: #2a2a2a; /* Visible border color */ color-scheme: dark; } @@ -784,11 +999,13 @@ p { color: var(--link); } -.results a:visited h3 { +.results a:visited h3, +.result_sublink a:visited h3 { color: var(--link-visited); } -.results h3 { +.results h3, +.result_sublink h3 { margin-top: 4px; margin-bottom: 2px; font-size: 20px; @@ -797,14 +1014,18 @@ p { letter-spacing: -0.4px; } -.results p { +.results p, +.result_sublink p { margin-top: 0px; color: var(--font-fg); } .results a, .results a:hover, -.results a:visited { +.results a:visited, +.result_sublink a, +.result_sublink a:hover, +.result_sublink a:visited { text-decoration: none; /* color: #ebecf7; */ font-size: 14px; @@ -822,6 +1043,113 @@ p { white-space: nowrap; } +.snip { + padding: 17px; + border: 1px solid var(--snip-border); + width: 440px; + border-radius: 8px; + background: var(--snip-background); + color: var(--fg); + margin-left: 840px; + margin-top: 0px; + position: absolute; + max-width: 48em; + line-height: 1.58; + font-size: 14px; + letter-spacing: normal; +} + +.snip_title { + margin-top: 0; + margin-bottom: 0; + color: var(--highlight); + font-weight: bold; + font-style: normal; + max-width: 48em; + line-height: 1.58; + font-size: 22px; +} + +.wiki_known_for { + margin: 0px; + font-weight: normal; + margin-bottom: 10px; +} + +.snip img { + float: right; + max-width: 30%; + max-height: 150px; + padding-bottom: 10px; + margin-left: 10px; +} + +.snip .wiki_link { + margin-top: 10px; + text-decoration: none; + color: var(--link); + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.snip a:hover { + text-decoration: underline; +} + +.snip .about { + font-size: 18px; + margin-top: 10px; + margin-bottom: 5px; +} + +.factpoint { + color: var(--fg); + font-weight: bold; + vertical-align: text-top; + text-align: left; + padding-right: 14px; +} + +.fact a { + color: var(--link); + text-decoration: none; +} +.fact a:visited { + color: var(--link-visited); +} + +.snipp { + padding: 10px; + border-bottom: 1px solid var(--border); + width: 500px; + background: inherit; + color: var(--fg); + margin-left: 170px; + margin-top: 0px; + position: relative; + line-height: 1.58; + font-size: 16px; + float: none; + max-width: 48em; +} + +.snipp a { + display: block; + margin-top: 10px; + text-decoration: none; + color: var(--link); + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.snipp a:hover { + text-decoration: underline; +} + .clickable { cursor: pointer; } @@ -833,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; @@ -843,93 +1172,159 @@ p { margin-left: 175px; } -/* Favicon styling */ -.result_header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} +/* MAP */ -.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 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; -} - -body, -h1, -p, -a, -input, -button { +.message { + position: absolute; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + padding: 10px; + background-color: var(--search-bg); + border: 1px solid var(--border); + border-radius: 5px; + box-shadow: 0 0 10px var(--box-shadow); + z-index: 1000; + width: auto; + max-width: 80%; + text-align: center; color: var(--text-color); - /* Applies the text color based on theme */ - background-color: var(--background-color); - /* Applies the background color based on theme */ +} + +/* Map container */ +#map { + height: calc(100% - 60px); + width: 100%; + top: 60px; +} + +/* Leaflet control buttons */ +.leaflet-control-locate, +.leaflet-control-layers-toggle { + background-color: var(--button) !important; + border: 1px solid var(--border) !important; + color: var(--fg) !important; +} + +.leaflet-bar a, +.leaflet-bar a:hover { + background-color: var(--button) !important; + border: 1px solid var(--border) !important; + color: var(--fg) !important; +} + +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: var(--html-bg) !important; + color: var(--text-color) !important; + border: 1px solid var(--border) !important; +} + +.leaflet-popup-content-wrapper a { + color: var(--link) !important; +} + +.leaflet-popup-content-wrapper a:hover { + text-decoration: underline; +} + +/* Leaflet control buttons */ +.leaflet-control-locate, +.leaflet-control-layers-toggle { + background-color: var(--button) !important; + border: 1px solid var(--border) !important; + color: var(--fg) !important; +} + +.leaflet-bar a, +.leaflet-bar a:hover { + background-color: var(--button) !important; + border: 1px solid var(--border) !important; + color: var(--fg) !important; +} + +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: var(--html-bg) !important; + color: var(--text-color) !important; + border: 1px solid var(--border) !important; +} + +.leaflet-popup-content-wrapper a { + color: var(--link) !important; +} + +.leaflet-popup-content-wrapper a:hover { + text-decoration: underline; +} + +/* Leaflet attribution control */ +.leaflet-control-attribution { + background: var(--button) !important; + border: 1px solid var(--border) !important; + color: var(--fg) !important; +} + +.leaflet-control-attribution a { + color: var(--link) !important; +} + +.leaflet-control-attribution a:hover { + text-decoration: underline !important; +} + +/* Leaflet scale control */ +.leaflet-control-scale { + background: var(--button) !important; + border: 1px solid var(--border) !important; + color: var(--fg) !important; + text-shadow: 1px 1px 2px var(--border) !important; /* Adjust text shadow */ +} + +.leaflet-control-scale-line { + background: var(--button) !important; + border: 1px solid var(--border) !important; + color: var(--fg) !important; + text-shadow: 1px 1px 2px var(--border) !important; /* Adjust text shadow */ +} + +.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); +} + +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 */ +} + +@media only screen and (max-width: 1320px) { + + .snip { + margin-left: 780px; + } +} + +@media only screen and (max-width: 1220px) { + + .snip { + position: relative; + float: none; + margin-left: 170px; + } } @media only screen and (max-width: 880px) { @@ -938,6 +1333,7 @@ button { margin-top: -15px !important; } + .snip, .no-results-found { position: relative; float: none; @@ -947,7 +1343,12 @@ button { font-size: 13px; } - .results h3 { + .calc { + left: 20px; + } + + .results h3, + .result_sublink h3 { font-size: 16px; } @@ -961,6 +1362,14 @@ button { height: 77px; } + .snip a { + font-size: 13px; + } + + .snip_title { + font-size: 16px; + } + .settings { max-width: 100%; margin-left: 0px; @@ -1025,6 +1434,20 @@ button { max-width: 92%; } + .check { + left: 20px; + max-width: 90%; + } + + .snipp { + position: relative; + float: none; + max-width: 90%; + margin-left: 10px; + width: auto; + font-size: 16px; + } + p { font-size: 13px; line-height: normal; @@ -1038,6 +1461,10 @@ button { font-size: 18px; } + .sublinks { + max-width: 90%; + } + .results-search-container { margin-left: auto; margin-right: auto; @@ -1050,6 +1477,19 @@ button { margin-left: 3px; } + .image_view { + width: 100%; + height: 77%; + margin-top: -33px; + margin-right: 0%; + border-top-right-radius: 0px; + border-top-left-radius: 0px; + } + + .view-image img { + max-height: 250px; + } + .images { margin-right: 1.2% !important; } @@ -1068,8 +1508,7 @@ button { padding: 10px; display: block; margin-top: 0px; - top: -5px; - /* how ? why ? I dont even wanna know */ + top: -5px; /* how ? why ? I dont even wanna know */ left: 0px; } @@ -1078,6 +1517,10 @@ button { height: 35px; } + .mobile-none { + display: none; + } + .sub-search-button-wrapper { margin: 0; padding: 0; @@ -1133,6 +1576,11 @@ button { max-width: 90%; } + .result_sublink { + left: 20px; + max-width: 90%; + } + .fetched { left: 20px; max-width: 90%; @@ -1140,20 +1588,14 @@ button { margin-top: -2px; } - .fetched_vid { - margin-top: 25px !important; - } - - .fetched_dif_videos { + .fetched_img { margin-top: 135px !important; + margin-left: 1.2% !important; + left: 0px !important; } - .fetched_dif_files { - margin-top: 25px !important; - } - - .fetched_dif_images { - margin-top: 25px; + .fetched_vid { + margin-top: 135px !important; } .results_settings { @@ -1167,7 +1609,6 @@ button { } form.torrent-sort { - margin-top: 35px; left: 20px; } @@ -1193,6 +1634,10 @@ button { margin-left: 3px; } + .search-button-wrapper button { + display: none; + } + .icon-button { margin-top: 30px; } @@ -1205,6 +1650,10 @@ button { margin-top: 30px; } + #clearSearch { + top: 6px; + } + } @media only screen and (max-width: 470px) { @@ -1218,8 +1667,7 @@ button { padding-right: 10px; position: relative; scroll-behavior: smooth; - scrollbar-width: none; - /* Hide scrollbar for Firefox */ + scrollbar-width: none; /* Hide scrollbar for Firefox */ } /* Hide scrollbar for Webkit-based browsers (e.g., Chrome, Safari) */ @@ -1229,20 +1677,15 @@ button { /* Gradient as a fixed visual indicator */ .sub-search-button-wrapper::after { - content: ''; - /* Adds a gradient or fade effect as a scroll indicator */ - position: fixed; - /* Ensure it doesn't move with scrolling */ + content: ''; /* Adds a gradient or fade effect as a scroll indicator */ + position: fixed; /* Ensure it doesn't move with scrolling */ top: 90px; right: 0; height: 40px; width: 20px; - background: linear-gradient(to left, var(--html-bg), rgba(255, 255, 255, 0)); - /* Adjust colors */ - pointer-events: none; - /* Prevent interaction */ - z-index: 1; - /* Ensure it is above the scrollable content */ + background: linear-gradient(to left, var(--html-bg), rgba(255, 255, 255, 0)); /* Adjust colors */ + pointer-events: none; /* Prevent interaction */ + z-index: 1; /* Ensure it is above the scrollable content */ } /* Hide gradient when fully scrolled */ @@ -1251,7 +1694,7 @@ button { } } -/* This is really bad */ + /* This is really bad */ @media only screen and (max-width: 400px) { .icon-button { @@ -1259,7 +1702,29 @@ button { } .results_settings { - display: flex; /* Enable flexbox */ - flex-wrap: wrap; /* Allow elements to wrap onto new lines when necessary */ + display: flex; /* Enable flexbox */ + flex-wrap: wrap; /* Allow elements to wrap onto new lines when necessary */ + } +} + +/* Ensuring dark theme compliance */ +@media (prefers-color-scheme: dark) { + .leaflet-control-locate, + .leaflet-control-layers-toggle, + .leaflet-bar a, + .leaflet-bar a:hover, + .leaflet-popup-content-wrapper, + .leaflet-popup-tip, + .leaflet-control-attribution, + .leaflet-control-scale, + .leaflet-control-scale-line { + background-color: var(--button) !important; + border: 1px solid var(--border) !important; + color: var(--fg) !important; + text-shadow: 1px 1px 2px var(--background-color) !important; /* Dark theme shadow adjustment */ + } + + .leaflet-control-attribution a { + color: var(--link) !important; } } \ 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/images/icon-512.svg b/static/images/icon-512.svg deleted file mode 100644 index 3e0b851..0000000 --- a/static/images/icon-512.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/static/js/calculator.js b/static/js/calculator.js deleted file mode 100644 index 71fc552..0000000 --- a/static/js/calculator.js +++ /dev/null @@ -1,53 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - const calcContainer = document.getElementById('dynamic-calc'); - const staticCalc = document.getElementById('static-calc'); - - if (calcContainer) { - calcContainer.style.display = 'block'; - if (staticCalc) staticCalc.style.display = 'none'; - - const input = document.getElementById('calc-input'); - const history = document.getElementById('calc-history'); - const buttons = document.querySelectorAll('.calc-buttons button'); - - const staticCalcContent = staticCalc?.textContent.trim(); // load HTML calc - const initialQuery = input.value.trim(); - - let currentInput = initialQuery; - let historyLog = []; - - if (initialQuery && staticCalcContent) { - historyLog.push(`${initialQuery} = ${staticCalcContent}`); - } - - const updateUI = () => { - input.value = currentInput; - history.innerHTML = historyLog.map(entry => `
${entry}
`).join(''); - }; - - buttons.forEach(button => { - button.addEventListener('click', () => { - const val = button.getAttribute('data-value'); - - if (val === 'C') { - currentInput = ''; - } else if (val === '=') { - try { - const res = eval(currentInput); - historyLog.push(`${currentInput} = ${res}`); - if (historyLog.length > 30) historyLog.shift(); // Remove oldest - currentInput = res.toString(); - } catch (e) { - currentInput = 'Error'; - } - } else { - currentInput += val; - } - - updateUI(); - }); - }); - - updateUI(); - } -}); diff --git a/static/js/dynamicscrolling.js b/static/js/dynamicscrolling.js index f648454..fb1ed87 100644 --- a/static/js/dynamicscrolling.js +++ b/static/js/dynamicscrolling.js @@ -1,58 +1,61 @@ /* This script is responsible for fetching new results on the same page when the user scrolls to the bottom of the page. */ -document.addEventListener("DOMContentLoaded", function () { +document.addEventListener("DOMContentLoaded", function() { const templateData = document.getElementById('template-data'); let page = parseInt(templateData.getAttribute('data-page')) || 1; const query = templateData.getAttribute('data-query') || ''; 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 || hasEndOrNoResultsMessage()) return; - + if (loading || !hasMoreResults) return; loading = true; - loadingTimeout = setTimeout(() => showLoadingMessage(), 150); + + // Show loading indicator if taking more than 150ms + loadingTimeout = setTimeout(() => { + loadingIndicator.style.display = 'flex'; + }, 150); fetch(`/search?q=${encodeURIComponent(query)}&t=${encodeURIComponent(searchType)}&p=${newPage}`) .then(response => { - if (!response.ok) throw new Error('Network response was not ok'); + if (!response.ok) { + throw new Error('Network response was not ok'); + } return response.text(); }) .then(data => { clearTimeout(loadingTimeout); - hideLoadingMessage(); - + loadingIndicator.style.display = 'none'; const parser = new DOMParser(); const doc = parser.parseFromString(data, 'text/html'); - const newResults = doc.querySelectorAll('#results > *'); - const resultsContainer = document.getElementById('results'); + const newResultsHTML = doc.getElementById('results').innerHTML; + const noResultsMessage = `No results found for '${query}'. Try different keywords.`; + const endOfResultsMessage = "Looks like this is the end of results."; + const serverError = "Internal Server Error"; - if (newResults.length === 0) { - hasMoreResults = false; - loading = false; - return; + const resultsContainer = document.getElementById('results'); + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = newResultsHTML; + while (tempDiv.firstChild) { + resultsContainer.appendChild(tempDiv.firstChild); } - newResults.forEach(el => resultsContainer.appendChild(el)); - page = newPage; - checkIfMoreResultsNeeded(); + if (newResultsHTML.includes(noResultsMessage) || newResultsHTML.includes(endOfResultsMessage) || newResultsHTML.includes(serverError)) { + hasMoreResults = false; + } else { + page = newPage; + // Automatically load more results if content height is less than window height + checkIfMoreResultsNeeded(); + } loading = false; }) .catch(error => { clearTimeout(loadingTimeout); - hideLoadingMessage(); + loadingIndicator.style.display = 'none'; console.error('Error loading results:', error); hasMoreResults = false; loading = false; @@ -60,17 +63,11 @@ document.addEventListener("DOMContentLoaded", function () { } function checkIfMoreResultsNeeded() { - if (!hasMoreResults || hasEndOrNoResultsMessage()) return; - - if (document.body.scrollHeight <= window.innerHeight) { + if (document.body.scrollHeight <= window.innerHeight && hasMoreResults) { loadResults(page + 1); } } - function hasEndOrNoResultsMessage() { - return !!document.querySelector('.no-results-found'); - } - window.addEventListener('scroll', () => { if (window.innerHeight + window.scrollY >= document.body.offsetHeight) { loadResults(page + 1); @@ -83,7 +80,7 @@ document.addEventListener("DOMContentLoaded", function () { console.error("No search buttons found"); } else { buttons.forEach(btn => { - btn.addEventListener('click', function () { + btn.addEventListener('click', function() { const activeElement = document.querySelector('.search-container-results-btn .search-active'); if (activeElement) { activeElement.classList.remove('search-active'); 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 ec6127d..ff35355 100755 --- a/templates/files.html +++ b/templates/files.html @@ -1,6 +1,5 @@ - @@ -12,54 +11,45 @@ - - - - + -
-

{{ translate "settings" }}

+

Settings

- +

Current theme: {{.Theme}}

-
Dark Theme
-
Light Theme
+
Dark Theme
+
Light Theme
+
@@ -71,9 +61,12 @@