diff --git a/build/buildmacos.sh b/build/buildmacos.sh index 725e996..6260e34 100755 --- a/build/buildmacos.sh +++ b/build/buildmacos.sh @@ -4,56 +4,133 @@ set -e # Variables -OUTPUT_DIR="../build" # Adjusted to place the binary in the build directory -BINARY_NAME="goanime-apple-darwin" -BINARY_PATH="$OUTPUT_DIR/$BINARY_NAME" -TARBALL_NAME="$BINARY_NAME.tar.gz" -TARBALL_PATH="$OUTPUT_DIR/$TARBALL_NAME" -CHECKSUM_FILE="$TARBALL_PATH.sha256" +OUTPUT_DIR="../build" # Adjusted to place the binaries in the build directory +BINARY_NAME_AMD64="goanime-darwin-amd64" +BINARY_NAME_ARM64="goanime-darwin-arm64" +BINARY_NAME_UNIVERSAL="goanime-darwin-universal" +BINARY_NAME_UNIVERSAL_GENERIC="goanime-darwin" +BINARY_PATH_AMD64="$OUTPUT_DIR/$BINARY_NAME_AMD64" +BINARY_PATH_ARM64="$OUTPUT_DIR/$BINARY_NAME_ARM64" +BINARY_PATH_UNIVERSAL="$OUTPUT_DIR/$BINARY_NAME_UNIVERSAL" +BINARY_PATH_UNIVERSAL_GENERIC="$OUTPUT_DIR/$BINARY_NAME_UNIVERSAL_GENERIC" MAIN_PACKAGE="../cmd/goanime" # Create the output directory if it doesn't exist mkdir -p "$OUTPUT_DIR" -echo "Building the goanime binary for macOS..." -CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o "$BINARY_PATH" -ldflags="-s -w" -trimpath "$MAIN_PACKAGE" +echo "Building goanime binaries for macOS..." -echo "Build completed: $BINARY_PATH" +# Build for Intel (amd64) +echo "Building for Intel (amd64)..." +CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o "$BINARY_PATH_AMD64" -ldflags="-s -w" -trimpath "$MAIN_PACKAGE" +echo "Intel build completed: $BINARY_PATH_AMD64" -# Check if UPX is installed -if command -v upx >/dev/null 2>&1; then - echo "Compressing the binary with UPX..." - if upx --best --ultra-brute --force-macos "$BINARY_PATH" 2>/dev/null; then - echo "Compression completed." - else - echo "UPX compression failed for macOS binary. Continuing without compression." - fi +# Build for Apple Silicon (arm64) +echo "Building for Apple Silicon (arm64)..." +CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o "$BINARY_PATH_ARM64" -ldflags="-s -w" -trimpath "$MAIN_PACKAGE" +echo "Apple Silicon build completed: $BINARY_PATH_ARM64" + +# Create universal binary using lipo +echo "Creating universal binary..." +if command -v lipo >/dev/null 2>&1; then + lipo -create -output "$BINARY_PATH_UNIVERSAL" "$BINARY_PATH_AMD64" "$BINARY_PATH_ARM64" + echo "Universal binary created: $BINARY_PATH_UNIVERSAL" + + # Create a copy with generic name for updater compatibility + cp "$BINARY_PATH_UNIVERSAL" "$BINARY_PATH_UNIVERSAL_GENERIC" + echo "Generic universal binary created: $BINARY_PATH_UNIVERSAL_GENERIC" else - echo "UPX not found. Skipping compression." + echo "Warning: lipo command not found. Cannot create universal binary." fi -# Check if the binary was built successfully -if [ ! -f "$BINARY_PATH" ]; then - echo "Error: Binary not found at $BINARY_PATH. Build may have failed." - exit 1 +# Function to compress binary with UPX +compress_binary() { + local binary_path="$1" + local binary_name=$(basename "$binary_path") + + if command -v upx >/dev/null 2>&1; then + echo "Compressing $binary_name with UPX..." + if upx --best --ultra-brute --force-macos "$binary_path" 2>/dev/null; then + echo "Compression completed for $binary_name." + else + echo "UPX compression failed for $binary_name. Continuing without compression." + fi + else + echo "UPX not found. Skipping compression for $binary_name." + fi +} + +# Compress binaries +compress_binary "$BINARY_PATH_AMD64" +compress_binary "$BINARY_PATH_ARM64" +if [ -f "$BINARY_PATH_UNIVERSAL" ]; then + compress_binary "$BINARY_PATH_UNIVERSAL" +fi +if [ -f "$BINARY_PATH_UNIVERSAL_GENERIC" ]; then + compress_binary "$BINARY_PATH_UNIVERSAL_GENERIC" fi +# Check if binaries were built successfully +for binary in "$BINARY_PATH_AMD64" "$BINARY_PATH_ARM64"; do + if [ ! -f "$binary" ]; then + echo "Error: Binary not found at $binary. Build may have failed." + exit 1 + fi +done -# Create tarball -echo "Creating tarball..." -tar -czf "$TARBALL_PATH" -C "$OUTPUT_DIR" "$BINARY_NAME" -echo "Tarball created: $TARBALL_PATH" +# Function to create tarball and checksum +create_tarball_and_checksum() { + local binary_path="$1" + local binary_name=$(basename "$binary_path") + local tarball_name="$binary_name.tar.gz" + local tarball_path="$OUTPUT_DIR/$tarball_name" + local checksum_file="$tarball_path.sha256" + + # Create tarball + echo "Creating tarball for $binary_name..." + tar -czf "$tarball_path" -C "$OUTPUT_DIR" "$binary_name" + echo "Tarball created: $tarball_path" + + # Generate SHA256 checksum for the tarball + echo "Generating SHA256 checksum for $tarball_name..." + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$tarball_path" > "$checksum_file" + elif command -v openssl >/dev/null 2>&1; then + openssl dgst -sha256 "$tarball_path" | awk '{print $2}' > "$checksum_file" + else + echo "Neither shasum nor openssl is available. Cannot generate checksum for $tarball_name." + return 1 + fi + echo "Checksum generated: $checksum_file" +} -# Generate SHA256 checksum for the tarball -echo "Generating SHA256 checksum for the tarball..." -if command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$TARBALL_PATH" > "$CHECKSUM_FILE" -elif command -v openssl >/dev/null 2>&1; then - openssl dgst -sha256 "$TARBALL_PATH" | awk '{print $2}' > "$CHECKSUM_FILE" -else - echo "Neither shasum nor openssl is available. Cannot generate checksum." - exit 1 +# Create tarballs and checksums for all binaries +create_tarball_and_checksum "$BINARY_PATH_AMD64" +create_tarball_and_checksum "$BINARY_PATH_ARM64" +if [ -f "$BINARY_PATH_UNIVERSAL" ]; then + create_tarball_and_checksum "$BINARY_PATH_UNIVERSAL" +fi +if [ -f "$BINARY_PATH_UNIVERSAL_GENERIC" ]; then + create_tarball_and_checksum "$BINARY_PATH_UNIVERSAL_GENERIC" +fi + +echo "Build script completed successfully. Generated binaries:" +echo "- Intel (amd64): $BINARY_PATH_AMD64" +echo "- Apple Silicon (arm64): $BINARY_PATH_ARM64" +if [ -f "$BINARY_PATH_UNIVERSAL" ]; then + echo "- Universal (explicit): $BINARY_PATH_UNIVERSAL" +fi +if [ -f "$BINARY_PATH_UNIVERSAL_GENERIC" ]; then + echo "- Universal (generic): $BINARY_PATH_UNIVERSAL_GENERIC" fi -echo "Checksum generated: $CHECKSUM_FILE" -echo "Build script completed successfully." +echo "" +echo "GitHub Release Assets:" +echo "- goanime-darwin-amd64 (Intel macOS)" +echo "- goanime-darwin-arm64 (Apple Silicon macOS)" +if [ -f "$BINARY_PATH_UNIVERSAL" ]; then + echo "- goanime-darwin-universal (Universal macOS - explicit)" +fi +if [ -f "$BINARY_PATH_UNIVERSAL_GENERIC" ]; then + echo "- goanime-darwin (Universal macOS - fallback for updater)" +fi diff --git a/docs/SCRAPING_INTEGRATION.md b/docs/SCRAPING_INTEGRATION.md index dfe12c3..378d551 100644 --- a/docs/SCRAPING_INTEGRATION.md +++ b/docs/SCRAPING_INTEGRATION.md @@ -2,7 +2,7 @@ This integration adds powerful web scraping capabilities to GoAnime, inspired by the popular `ani-cli` script. It supports multiple anime streaming sources with automatic fallback and enhanced download features. -## 🌟 New Features +## New Features ### Multi-Source Support - **AllAnime.day**: High-quality streams with multiple resolution options @@ -22,7 +22,7 @@ This integration adds powerful web scraping capabilities to GoAnime, inspired by - **720p, 1080p, 480p**: Specific resolution selection - **hls**: HLS/m3u8 streams for better compatibility -## 🚀 Usage Examples +## Usage Examples ### Basic Usage ```bash @@ -51,7 +51,7 @@ goanime -d --source animefire "naruto" 25 goanime -d --quality best "bleach" 100 ``` -## 🔧 Technical Implementation +## Technical Implementation ### Architecture Overview ``` @@ -84,21 +84,7 @@ type UnifiedScraper interface { 5. **Error Handling with Fallbacks** 6. **Metadata Extraction** -## 🔄 Migration from ani-cli -This integration brings many features from the popular `ani-cli` bash script: - -### Supported ani-cli Features -- [x] Multi-source anime search -- [x] Quality selection (best, worst, specific resolutions) -- [x] Episode range downloads -- [x] HLS/m3u8 stream support -- [x] Subtitle extraction -- [x] Referrer handling for protected streams -- [x] User-agent spoofing -- [ ] Skip intro functionality (planned) -- [ ] Syncplay support (planned) -- [ ] VLC integration (planned) ### Command Equivalents ```bash @@ -114,7 +100,7 @@ goanime -d -r "anime name" 1-5 goanime -d --quality 720p "anime name" 1 ``` -## 📋 Configuration +## Configuration ### Environment Variables ```bash @@ -161,7 +147,7 @@ Enable verbose logging to troubleshoot issues: goanime --debug -d --source allanime "your anime" 1 ``` -## 🔮 Future Enhancements +## Future Enhancements ### Planned Features - [ ] Additional streaming sources @@ -181,7 +167,7 @@ goanime --debug -d --source allanime "your anime" 1 - [ ] Web UI for remote control - [ ] Mobile app companion -## 🤝 Contributing +## Contributing To add a new anime source: @@ -203,10 +189,3 @@ func (c *NewSourceClient) SearchAnime(query string, options ...interface{}) ([]* } ``` -## 📄 License - -This enhanced scraping functionality is part of the GoAnime project and follows the same license terms. The implementation is inspired by `ani-cli` but written from scratch in Go. - ---- - -**Note**: Always respect the terms of service of the streaming sites you're accessing. This tool is for educational purposes and personal use only. diff --git a/go.mod b/go.mod index 3e55222..8ba295d 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,38 @@ module github.com/alvarorichard/Goanime -go 1.24.5 +go 1.25 require ( github.com/Microsoft/go-winio v0.6.2 github.com/PuerkitoBio/goquery v1.10.3 github.com/alvarorichard/rich-go v0.0.0-20250531060310-d14b9a86fb85 github.com/charmbracelet/bubbles v0.21.0 - github.com/charmbracelet/bubbletea v1.3.6 + github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/ktr0731/go-fuzzyfinder v0.9.0 github.com/manifoldco/promptui v0.9.0 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.10.0 - golang.org/x/net v0.43.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/net v0.46.0 ) require ( github.com/charmbracelet/huh v0.7.0 github.com/charmbracelet/log v0.4.2 - github.com/lrstanley/go-ytdlp v1.2.2 + github.com/lrstanley/go-ytdlp v1.2.6 ) require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20250820142022-371acb6ebad9 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20251013190359-01371c2be815 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logfmt/logfmt v0.6.1 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect - github.com/ulikunitz/xz v0.5.13 // indirect - golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect ) require ( @@ -40,7 +41,7 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.3.2 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/charmbracelet/x/ansi v0.10.1 // indirect; indirectn + github.com/charmbracelet/x/ansi v0.10.2 // indirect; indirectn github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/chzyer/readline v1.5.1 // indirect @@ -48,12 +49,12 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/gdamore/encoding v1.0.1 // indirect - github.com/gdamore/tcell/v2 v2.8.1 // indirect + github.com/gdamore/tcell/v2 v2.9.0 // indirect github.com/ktr0731/go-ansisgr v0.1.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mattn/go-sqlite3 v1.14.32 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect @@ -61,13 +62,12 @@ require ( github.com/nsf/termbox-go v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/objx v0.5.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 711237f..e1a6a72 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= -github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= @@ -32,8 +32,8 @@ github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoF github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig= github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw= -github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= -github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= +github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= @@ -42,8 +42,12 @@ github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9 github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/strings v0.0.0-20250820142022-371acb6ebad9 h1:IhpkTLJ0YN+PlxCeCGbapaKQpeNId3KXQaktWSK05zA= -github.com/charmbracelet/x/exp/strings v0.0.0-20250820142022-371acb6ebad9/go.mod h1:Rgw3/F+xlcUc5XygUtimVSxAqCOsqyvJjqF5UHRvc5k= +github.com/charmbracelet/x/exp/strings v0.0.0-20250930200525-31788bbe6486 h1:GQnuszem7BRop8SWwv7G3D304AmlncaWjTc+KMjA8OY= +github.com/charmbracelet/x/exp/strings v0.0.0-20250930200525-31788bbe6486/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= +github.com/charmbracelet/x/exp/strings v0.0.0-20251008171431-5d3777519489 h1:hqD5qznOKYam7T7t1iRpm+tkXUkCDbXRAELeFm6fThs= +github.com/charmbracelet/x/exp/strings v0.0.0-20251008171431-5d3777519489/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= +github.com/charmbracelet/x/exp/strings v0.0.0-20251013190359-01371c2be815 h1:BkqB3Fyp/ix8ejltmBx/qp2AjKWanb4b0N7iO2BL6xQ= +github.com/charmbracelet/x/exp/strings v0.0.0-20251013190359-01371c2be815/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= @@ -59,6 +63,8 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -72,10 +78,12 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= -github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= -github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= +github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys= +github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= +github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -92,10 +100,10 @@ github.com/ktr0731/go-ansisgr v0.1.0 h1:fbuupput8739hQbEmZn1cEKjqQFwtCCZNznnF6AN github.com/ktr0731/go-ansisgr v0.1.0/go.mod h1:G9lxwgBwH0iey0Dw5YQd7n6PmQTwTuTM/X5Sgm/UrzE= github.com/ktr0731/go-fuzzyfinder v0.9.0 h1:JV8S118RABzRl3Lh/RsPhXReJWc2q0rbuipzXQH7L4c= github.com/ktr0731/go-fuzzyfinder v0.9.0/go.mod h1:uybx+5PZFCgMCSDHJDQ9M3nNKx/vccPmGffsXPn2ad8= -github.com/lrstanley/go-ytdlp v1.2.2 h1:bBFDDlEUYkEMwbtOrUVfU4qBf3PbDJWgCER2pU60TSE= -github.com/lrstanley/go-ytdlp v1.2.2/go.mod h1:4Mwvk8i5dAeeBDAEoxeJLa46xA/YpkzO5M6zg7MHJa0= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lrstanley/go-ytdlp v1.2.6 h1:LJ1I+uaP2KviRAfe3tUN0Sd4yI9XlCJBG37RCH+sfq8= +github.com/lrstanley/go-ytdlp v1.2.6/go.mod h1:38IL64XM6gULrWtKTiR0+TTNCVbxesNSbTyaFG2CGTI= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -103,8 +111,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= @@ -122,18 +130,18 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -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/ulikunitz/xz v0.5.13 h1:ar98gWrjf4H1ev05fYP/o29PDZw9DrI3niHtnEqyuXA= -github.com/ulikunitz/xz v0.5.13/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -143,10 +151,16 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU= +golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A= +golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA= +golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -161,8 +175,10 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -170,8 +186,6 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -187,9 +201,10 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -199,9 +214,10 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -211,8 +227,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/api/enhanced.go b/internal/api/enhanced.go index 4c1e0d4..9b40d6a 100644 --- a/internal/api/enhanced.go +++ b/internal/api/enhanced.go @@ -21,12 +21,15 @@ func SearchAnimeEnhanced(name string, source string) (*models.Anime, error) { if strings.ToLower(source) == "allanime" { t := scraper.AllAnimeType scraperType = &t + util.Debug("Searching specific source", "source", "AllAnime") } else if strings.ToLower(source) == "animefire" { t := scraper.AnimefireType scraperType = &t + util.Debug("Searching specific source", "source", "AnimeFire") } else { // Default behavior: search both sources simultaneously scraperType = nil + util.Debug("Searching all sources", "query", name) } // Perform the search - this will search both sources if scraperType is nil diff --git a/internal/appflow/anime_data.go b/internal/appflow/anime_data.go index e8d2543..2a7eed3 100644 --- a/internal/appflow/anime_data.go +++ b/internal/appflow/anime_data.go @@ -1,6 +1,7 @@ package appflow import ( + "fmt" "log" "strings" "time" @@ -9,6 +10,7 @@ import ( "github.com/alvarorichard/Goanime/internal/models" "github.com/alvarorichard/Goanime/internal/util" + "github.com/charmbracelet/huh" ) func SearchAnime(name string) *models.Anime { @@ -38,6 +40,58 @@ func SearchAnimeEnhanced(name string) *models.Anime { return anime } +// SearchAnimeWithRetry - searches for anime with retry logic on failure +func SearchAnimeWithRetry(name string) (*models.Anime, error) { + const maxRetries = 3 + currentName := name + + for i := 0; i < maxRetries; i++ { + searchStart := time.Now() + + // Attempt to search for anime (empty string means search all sources) + util.Debugf("Search attempt %d/%d for: %s (searching all sources)", i+1, maxRetries, currentName) + anime, err := api.SearchAnimeEnhanced(currentName, "") + + if err == nil && anime != nil { + util.Debugf("[PERF] SearchAnimeWithRetry completed in %v", time.Since(searchStart)) + return anime, nil + } + + // Display error message to user + if i < maxRetries-1 { + util.Errorf("No anime found with the name: %s", currentName) + util.Infof("Please try again with a different search term.") + + // Prompt user for new input + var newName string + prompt := huh.NewInput(). + Title("Search Again"). + Description("Enter a new anime name to search for:"). + Value(&newName). + Validate(func(v string) error { + if len(strings.TrimSpace(v)) < 2 { + return fmt.Errorf("anime name must be at least 2 characters") + } + return nil + }) + + if promptErr := prompt.Run(); promptErr != nil { + return nil, fmt.Errorf("search cancelled by user") + } + + currentName = strings.TrimSpace(newName) + if currentName == "" { + return nil, fmt.Errorf("search cancelled: empty name provided") + } + } else { + // Last attempt failed + return nil, fmt.Errorf("failed to find anime after %d attempts", maxRetries) + } + } + + return nil, fmt.Errorf("failed to find anime after %d attempts", maxRetries) +} + func FetchAnimeDetails(anime *models.Anime) { detailsStart := time.Now() diff --git a/internal/download/workflow.go b/internal/download/workflow.go index 5b87cf7..8cb9a73 100644 --- a/internal/download/workflow.go +++ b/internal/download/workflow.go @@ -24,13 +24,11 @@ func HandleDownloadRequest(request *util.DownloadRequest) error { util.Infof("Using source: %s, quality: %s", source, quality) - // Try enhanced search first (supports multiple sources) - anime, err := api.SearchAnimeEnhanced(request.AnimeName, source) + // Try enhanced search with retry logic + anime, err := appflow.SearchAnimeWithRetry(request.AnimeName) if err != nil { - util.Infof("Enhanced search failed, falling back to legacy search: %v", err) - // Fallback to legacy search - anime = appflow.SearchAnime(request.AnimeName) - appflow.FetchAnimeDetails(anime) + util.Errorf("Failed to search for anime: %v", err) + return err } if request.IsRange { diff --git a/internal/handlers/playback.go b/internal/handlers/playback.go index 7fd5084..22f9f9d 100644 --- a/internal/handlers/playback.go +++ b/internal/handlers/playback.go @@ -28,8 +28,13 @@ func HandlePlaybackMode(animeName string) { defer discordManager.Shutdown() } - // Use enhanced search to find anime in both AllAnime and AnimeFire - anime := appflow.SearchAnimeEnhanced(animeName) + // Use enhanced search with retry logic + anime, err := appflow.SearchAnimeWithRetry(animeName) + if err != nil { + util.Errorf("Failed to search for anime: %v", err) + return + } + appflow.FetchAnimeDetails(anime) episodes := appflow.GetAnimeEpisodes(anime) diff --git a/internal/playback/movie.go b/internal/playback/movie.go index 99abfbe..059bb01 100644 --- a/internal/playback/movie.go +++ b/internal/playback/movie.go @@ -4,6 +4,8 @@ import ( "errors" "fmt" "log" + "os" + "path/filepath" "runtime" "sync" "time" @@ -164,5 +166,6 @@ func getSocketPath() string { if runtime.GOOS == "windows" { return `\\.\pipe\goanime_mpvsocket` } - return "/tmp/mpvsocket" + // Use os.TempDir() for macOS compatibility + return filepath.Join(os.TempDir(), "mpvsocket") } diff --git a/internal/playback/series.go b/internal/playback/series.go index 661f658..d34aba7 100644 --- a/internal/playback/series.go +++ b/internal/playback/series.go @@ -246,37 +246,50 @@ func CheckIfSeriesEnhanced(anime *models.Anime) (bool, int) { // ChangeAnimeLocal allows the user to search for and select a new anime (local implementation to avoid circular imports) func ChangeAnimeLocal() (*models.Anime, []models.Episode, error) { - var animeName string - - prompt := huh.NewInput(). - Title("Change Anime"). - Description("Enter the name of the anime you want to watch:"). - Value(&animeName) - - if err := prompt.Run(); err != nil { - return nil, nil, err - } - - if len(animeName) < 2 { - util.Errorf("Anime name too short") - return nil, nil, fmt.Errorf("anime name must be at least 2 characters") - } + const maxRetries = 3 + + for i := 0; i < maxRetries; i++ { + var animeName string + + prompt := huh.NewInput(). + Title("Change Anime"). + Description("Enter the name of the anime you want to watch:"). + Value(&animeName). + Validate(func(v string) error { + if len(v) < 2 { + return fmt.Errorf("anime name must be at least 2 characters") + } + return nil + }) + + if err := prompt.Run(); err != nil { + return nil, nil, err + } - // Use the enhanced API to search for anime - anime, err := api.SearchAnimeEnhanced(animeName, "") - if err != nil { - return nil, nil, fmt.Errorf("failed to search anime: %w", err) - } + // Use the enhanced API to search for anime + anime, err := api.SearchAnimeEnhanced(animeName, "") + if err != nil || anime == nil { + if i < maxRetries-1 { + util.Errorf("No anime found with the name: %s", animeName) + util.Infof("Please try again with a different search term. (Attempt %d/%d)", i+2, maxRetries) + continue + } + return nil, nil, fmt.Errorf("failed to find anime after %d attempts", maxRetries) + } - if anime == nil { - return nil, nil, fmt.Errorf("no anime found with name: %s", animeName) - } + // Get episodes for the new anime using enhanced API + episodes, err := api.GetAnimeEpisodesEnhanced(anime) + if err != nil { + if i < maxRetries-1 { + util.Errorf("Failed to get episodes for: %s", anime.Name) + util.Infof("Please try searching for a different anime. (Attempt %d/%d)", i+2, maxRetries) + continue + } + return nil, nil, fmt.Errorf("failed to get episodes after %d attempts", maxRetries) + } - // Get episodes for the new anime using enhanced API - episodes, err := api.GetAnimeEpisodesEnhanced(anime) - if err != nil { - return nil, nil, fmt.Errorf("failed to get episodes: %w", err) + return anime, episodes, nil } - return anime, episodes, nil + return nil, nil, fmt.Errorf("failed to change anime after %d attempts", maxRetries) } diff --git a/internal/player/player.go b/internal/player/player.go index 5e965eb..86c619c 100644 --- a/internal/player/player.go +++ b/internal/player/player.go @@ -75,7 +75,9 @@ func StartVideo(link string, args []string) (string, error) { if runtime.GOOS == "windows" { socketPath = fmt.Sprintf(`\\.\pipe\goanime_mpvsocket_%s`, randomNumber) } else { - tmpDir := "/tmp" + // Use os.TempDir() instead of hardcoded /tmp for macOS compatibility + // macOS uses /var/folders/... accessed via $TMPDIR + tmpDir := os.TempDir() if err := os.MkdirAll(tmpDir, 0700); err != nil { return "", fmt.Errorf("failed to create tmp directory: %w", err) } diff --git a/internal/scraper/animefire.go b/internal/scraper/animefire.go index a2fb2dd..27f1f14 100644 --- a/internal/scraper/animefire.go +++ b/internal/scraper/animefire.go @@ -2,14 +2,15 @@ package scraper import ( + "errors" "fmt" "net/http" - "net/url" "strings" "time" "github.com/PuerkitoBio/goquery" "github.com/alvarorichard/Goanime/internal/models" + "github.com/alvarorichard/Goanime/internal/util" ) const ( @@ -18,9 +19,11 @@ const ( // AnimefireClient handles interactions with Animefire.plus type AnimefireClient struct { - client *http.Client - baseURL string - userAgent string + client *http.Client + baseURL string + userAgent string + maxRetries int + retryDelay time.Duration } // NewAnimefireClient creates a new Animefire client @@ -29,84 +32,168 @@ func NewAnimefireClient() *AnimefireClient { client: &http.Client{ Timeout: 30 * time.Second, }, - baseURL: AnimefireBase, - userAgent: UserAgent, + baseURL: AnimefireBase, + userAgent: UserAgent, + maxRetries: 2, + retryDelay: 350 * time.Millisecond, } } // SearchAnime searches for anime on Animefire.plus using the original logic func (c *AnimefireClient) SearchAnime(query string) ([]*models.Anime, error) { - searchURL := fmt.Sprintf("%s/pesquisar/%s", c.baseURL, url.QueryEscape(query)) + // AnimeFire expects spaces as hyphens in the URL + normalizedQuery := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(query)), " ", "-") + searchURL := fmt.Sprintf("%s/pesquisar/%s", c.baseURL, normalizedQuery) - req, err := http.NewRequest("GET", searchURL, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) + util.Debug("AnimeFire search", "query", query, "normalized", normalizedQuery, "url", searchURL) + + var lastErr error + attempts := c.maxRetries + 1 + + for attempt := 0; attempt < attempts; attempt++ { + req, err := http.NewRequest("GET", searchURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + c.decorateRequest(req) + + resp, err := c.client.Do(req) + if err != nil { + lastErr = fmt.Errorf("failed to make request: %w", err) + if c.shouldRetry(attempt) { + c.sleep() + continue + } + return nil, lastErr + } + + if resp.StatusCode != http.StatusOK { + lastErr = c.handleStatusError(resp) + _ = resp.Body.Close() + if c.shouldRetry(attempt) { + c.sleep() + continue + } + return nil, lastErr + } + + doc, err := goquery.NewDocumentFromReader(resp.Body) + _ = resp.Body.Close() + if err != nil { + lastErr = fmt.Errorf("failed to parse HTML: %w", err) + if c.shouldRetry(attempt) { + c.sleep() + continue + } + return nil, lastErr + } + + if c.isChallengePage(doc) { + lastErr = errors.New("animefire returned a challenge page (try VPN or wait)") + if c.shouldRetry(attempt) { + c.sleep() + continue + } + return nil, lastErr + } + + animes := c.extractSearchResults(doc) + if len(animes) == 0 { + // Legitimate empty result set – return without error + return []*models.Anime{}, nil + } + + return animes, nil } + if lastErr != nil { + return nil, lastErr + } + return nil, errors.New("failed to retrieve results from AnimeFire") +} + +func (c *AnimefireClient) decorateRequest(req *http.Request) { req.Header.Set("User-Agent", c.userAgent) + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + req.Header.Set("Accept-Language", "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Pragma", "no-cache") + req.Header.Set("Referer", c.baseURL+"/") +} - resp, err := c.client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to make request: %w", err) +func (c *AnimefireClient) handleStatusError(resp *http.Response) error { + if resp.StatusCode == http.StatusForbidden { + return fmt.Errorf("access restricted: VPN may be required") } - defer func() { _ = resp.Body.Close() }() + return fmt.Errorf("server returned: %s", resp.Status) +} - if resp.StatusCode != http.StatusOK { - if resp.StatusCode == http.StatusForbidden { - return nil, fmt.Errorf("access restricted: VPN may be required") - } - return nil, fmt.Errorf("server returned: %s", resp.Status) +func (c *AnimefireClient) shouldRetry(attempt int) bool { + return attempt < c.maxRetries +} + +func (c *AnimefireClient) sleep() { + if c.retryDelay <= 0 { + return + } + time.Sleep(c.retryDelay) +} + +func (c *AnimefireClient) isChallengePage(doc *goquery.Document) bool { + title := strings.ToLower(strings.TrimSpace(doc.Find("title").First().Text())) + if strings.Contains(title, "just a moment") { + return true } - doc, err := goquery.NewDocumentFromReader(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to parse HTML: %w", err) + if doc.Find("#cf-wrapper").Length() > 0 || doc.Find("#challenge-form").Length() > 0 { + return true } + body := strings.ToLower(doc.Text()) + return strings.Contains(body, "cf-error") || strings.Contains(body, "cloudflare") +} + +func (c *AnimefireClient) extractSearchResults(doc *goquery.Document) []*models.Anime { var animes []*models.Anime - // Use the same parsing logic as the original system doc.Find(".row.ml-1.mr-1 a").Each(func(i int, s *goquery.Selection) { if urlPath, exists := s.Attr("href"); exists { name := strings.TrimSpace(s.Text()) if name != "" { - fullURL := c.resolveURL(c.baseURL, urlPath) - anime := &models.Anime{ + animes = append(animes, &models.Anime{ Name: name, - URL: fullURL, - } - animes = append(animes, anime) + URL: c.resolveURL(c.baseURL, urlPath), + }) } } }) - // If no results with the primary selector, try the card-based selector as fallback - if len(animes) == 0 { - doc.Find(".card_ani").Each(func(i int, s *goquery.Selection) { - titleElem := s.Find(".ani_name a") - title := strings.TrimSpace(titleElem.Text()) - link, exists := titleElem.Attr("href") - - if exists && title != "" { - // Get image URL - imgElem := s.Find(".div_img img") - imgURL, _ := imgElem.Attr("src") - if imgURL != "" { - imgURL = c.resolveURL(c.baseURL, imgURL) - } - - anime := &models.Anime{ - Name: title, - URL: c.resolveURL(c.baseURL, link), - ImageURL: imgURL, - } - - animes = append(animes, anime) - } - }) + if len(animes) > 0 { + return animes } - return animes, nil + doc.Find(".card_ani").Each(func(i int, s *goquery.Selection) { + titleElem := s.Find(".ani_name a") + title := strings.TrimSpace(titleElem.Text()) + link, exists := titleElem.Attr("href") + + if exists && title != "" { + imgElem := s.Find(".div_img img") + imgURL, _ := imgElem.Attr("src") + if imgURL != "" { + imgURL = c.resolveURL(c.baseURL, imgURL) + } + + animes = append(animes, &models.Anime{ + Name: title, + URL: c.resolveURL(c.baseURL, link), + ImageURL: imgURL, + }) + } + }) + + return animes } // resolveURL resolves relative URLs to absolute URLs diff --git a/internal/scraper/animefire_test.go b/internal/scraper/animefire_test.go new file mode 100644 index 0000000..86633fb --- /dev/null +++ b/internal/scraper/animefire_test.go @@ -0,0 +1,92 @@ +package scraper + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAnimefireSearchRetriesOnFailure(t *testing.T) { + t.Parallel() + + attempts := 0 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attempts++ + if attempts == 1 { + w.WriteHeader(http.StatusBadGateway) + return + } + + _, _ = fmt.Fprint(w, ` + +
+