Info Fetching completed
This commit is contained in:
		
							parent
							
								
									b5f8fb0118
								
							
						
					
					
						commit
						f129d1912b
					
				| 
						 | 
				
			
			@ -26,4 +26,4 @@ tmp/
 | 
			
		|||
# Logs
 | 
			
		||||
*.log
 | 
			
		||||
 | 
			
		||||
# Any other temporary or build files specific to your project
 | 
			
		||||
__*
 | 
			
		||||
| 
						 | 
				
			
			@ -35,13 +35,7 @@ func init() {
 | 
			
		|||
func runInfoCommand() error {
 | 
			
		||||
	album, err := bunkr.FetchAlbumInfo(albumURL)
 | 
			
		||||
 | 
			
		||||
	// Print
 | 
			
		||||
	fmt.Printf("Album Title: %s\n", album.Title)
 | 
			
		||||
	fmt.Printf("Album URL: %s\n", album.URL.String())
 | 
			
		||||
	fmt.Printf("Files (%d):\n", len(album.Files))
 | 
			
		||||
	for _, f := range album.Files {
 | 
			
		||||
		fmt.Printf(" - %s (%s)\n", f.Filename, f.URL.String())
 | 
			
		||||
	}
 | 
			
		||||
	fmt.Println(album.ToString())
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										7
									
								
								go.mod
								
								
								
								
							
							
						
						
									
										7
									
								
								go.mod
								
								
								
								
							| 
						 | 
				
			
			@ -3,10 +3,13 @@ module tea.chunkbyte.com/kato/grabrr
 | 
			
		|||
go 1.23.2
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/PuerkitoBio/goquery v1.10.3 // indirect
 | 
			
		||||
	github.com/PuerkitoBio/goquery v1.10.3
 | 
			
		||||
	github.com/spf13/cobra v1.9.1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/andybalholm/cascadia v1.3.3 // indirect
 | 
			
		||||
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 | 
			
		||||
	github.com/spf13/cobra v1.9.1 // indirect
 | 
			
		||||
	github.com/spf13/pflag v1.0.6 // indirect
 | 
			
		||||
	golang.org/x/net v0.39.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,9 +4,9 @@ import (
 | 
			
		|||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"tea.chunkbyte.com/kato/grabrr/lib/helper"
 | 
			
		||||
	strux "tea.chunkbyte.com/kato/grabrr/lib/strux"
 | 
			
		||||
 | 
			
		||||
	"github.com/PuerkitoBio/goquery"
 | 
			
		||||
| 
						 | 
				
			
			@ -39,9 +39,15 @@ func FetchAlbumInfo(rawURL string) (*strux.Album, error) {
 | 
			
		|||
		return nil, fmt.Errorf("failed to parse URL: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	album.ID = helper.GetLastPathElement(album.URL)
 | 
			
		||||
	// Title
 | 
			
		||||
	album.Title = strings.TrimSpace(doc.Find("h1.truncate").First().Text())
 | 
			
		||||
 | 
			
		||||
	// Views
 | 
			
		||||
	if albumViews, err := album.FetchAlbumViews(); err == nil {
 | 
			
		||||
		album.Views = albumViews
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Files
 | 
			
		||||
	doc.Find(".grid-images .theItem").Each(func(i int, s *goquery.Selection) {
 | 
			
		||||
		linkTag := s.Find("a")
 | 
			
		||||
| 
						 | 
				
			
			@ -58,7 +64,7 @@ func FetchAlbumInfo(rawURL string) (*strux.Album, error) {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if theSizeEl := s.Find(".theSize"); theSizeEl != nil {
 | 
			
		||||
			fileSize, err = ParseSizeToKB(theSizeEl.Text())
 | 
			
		||||
			fileSize, err = helper.ParseSizeToKB(theSizeEl.Text())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if href[0] == '/' {
 | 
			
		||||
| 
						 | 
				
			
			@ -72,38 +78,18 @@ func FetchAlbumInfo(rawURL string) (*strux.Album, error) {
 | 
			
		|||
			URL:      fileURL,
 | 
			
		||||
			Filename: fileTitle,
 | 
			
		||||
			SizeKB:   fileSize,
 | 
			
		||||
			Format:   fileURL.Path[strings.LastIndex(fileURL.Path, ".")+1:],
 | 
			
		||||
			Format:   helper.GetMimeTypeFromFilename(fileTitle),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		uploadTimeStampRaw := strings.TrimSpace(s.Find(".theDate").First().Text())
 | 
			
		||||
		if newDate, err := helper.ParseCustomDateTime(uploadTimeStampRaw); err == nil {
 | 
			
		||||
			file.DateUploaded = newDate
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		album.Files = append(album.Files, file)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	album.SizeKB = album.GetKbSize()
 | 
			
		||||
 | 
			
		||||
	return album, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseSizeToKB(sizeStr string) (int64, error) {
 | 
			
		||||
	s := strings.TrimSpace(strings.ToLower(sizeStr))
 | 
			
		||||
 | 
			
		||||
	var multiplier float64
 | 
			
		||||
	switch {
 | 
			
		||||
	case strings.HasSuffix(s, "gb"):
 | 
			
		||||
		multiplier = 1024 * 1024
 | 
			
		||||
		s = strings.TrimSuffix(s, "gb")
 | 
			
		||||
	case strings.HasSuffix(s, "mb"):
 | 
			
		||||
		multiplier = 1024
 | 
			
		||||
		s = strings.TrimSuffix(s, "mb")
 | 
			
		||||
	case strings.HasSuffix(s, "kb"):
 | 
			
		||||
		multiplier = 1
 | 
			
		||||
		s = strings.TrimSuffix(s, "kb")
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, fmt.Errorf("unrecognized unit in %q", sizeStr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s = strings.TrimSpace(s)
 | 
			
		||||
	value, err := strconv.ParseFloat(s, 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, fmt.Errorf("invalid number %q: %w", s, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return int64(value * multiplier), nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
package constants
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	MAIN_HOST_URL = "s.bunkr.ru"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,98 @@
 | 
			
		|||
package helper
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ParseSizeToKB(sizeStr string) (int64, error) {
 | 
			
		||||
	s := strings.TrimSpace(strings.ToLower(sizeStr))
 | 
			
		||||
 | 
			
		||||
	var multiplier float64
 | 
			
		||||
	switch {
 | 
			
		||||
	case strings.HasSuffix(s, "gb"):
 | 
			
		||||
		multiplier = 1024 * 1024
 | 
			
		||||
		s = strings.TrimSuffix(s, "gb")
 | 
			
		||||
	case strings.HasSuffix(s, "mb"):
 | 
			
		||||
		multiplier = 1024
 | 
			
		||||
		s = strings.TrimSuffix(s, "mb")
 | 
			
		||||
	case strings.HasSuffix(s, "kb"):
 | 
			
		||||
		multiplier = 1
 | 
			
		||||
		s = strings.TrimSuffix(s, "kb")
 | 
			
		||||
	default:
 | 
			
		||||
		return 0, fmt.Errorf("unrecognized unit in %q", sizeStr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s = strings.TrimSpace(s)
 | 
			
		||||
	value, err := strconv.ParseFloat(s, 64)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, fmt.Errorf("invalid number %q: %w", s, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return int64(value * multiplier), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func FormatSizeFromKB(sizeKB int64) (string, error) {
 | 
			
		||||
	if sizeKB < 0 {
 | 
			
		||||
		return "", fmt.Errorf("size must be non-negative")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	units := []string{"Kb", "Mb", "Gb", "Tb"} //Add more if needed.
 | 
			
		||||
	unitIndex := 0
 | 
			
		||||
 | 
			
		||||
	sizeFloat := float64(sizeKB)
 | 
			
		||||
 | 
			
		||||
	for sizeFloat >= 1024 && unitIndex < len(units)-1 {
 | 
			
		||||
		sizeFloat /= 1024
 | 
			
		||||
		unitIndex++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Sprintf("%.2f %s", sizeFloat, units[unitIndex]), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetLastPathElement(u *url.URL) string {
 | 
			
		||||
 | 
			
		||||
	path := u.Path
 | 
			
		||||
	if path == "" {
 | 
			
		||||
		return "" // Handle empty path
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	parts := strings.Split(path, "/")
 | 
			
		||||
	if len(parts) == 0 {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return parts[len(parts)-1]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetMimeTypeFromFilename(filename string) string {
 | 
			
		||||
	ext := strings.ToLower(filepath.Ext(filename))
 | 
			
		||||
	if ext == "" {
 | 
			
		||||
		return "application/octet-stream" // fallback
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mimeType := mime.TypeByExtension(ext)
 | 
			
		||||
	if mimeType != "" {
 | 
			
		||||
		return mimeType
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// handle common missing types
 | 
			
		||||
	switch ext {
 | 
			
		||||
	case ".mov":
 | 
			
		||||
		return "video/quicktime"
 | 
			
		||||
	case ".rar":
 | 
			
		||||
		return "application/x-rar-compressed"
 | 
			
		||||
	default:
 | 
			
		||||
		return "application/octet-stream"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseCustomDateTime(input string) (time.Time, error) {
 | 
			
		||||
	layout := "15:04:05 02/01/2006"
 | 
			
		||||
	return time.Parse(layout, input)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +1,22 @@
 | 
			
		|||
package structs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"tea.chunkbyte.com/kato/grabrr/lib/constants"
 | 
			
		||||
	"tea.chunkbyte.com/kato/grabrr/lib/helper"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// File represents a single file within a Bunkkr album.
 | 
			
		||||
type File struct {
 | 
			
		||||
	Title        string    `json:"title"`
 | 
			
		||||
	SizeKB       int64     `json:"size_kb"`
 | 
			
		||||
	DateUploaded time.Time `json:"date_uploaded"`
 | 
			
		||||
	URL          *url.URL  `json:"url"`
 | 
			
		||||
	Filename     string    `json:"filename"`
 | 
			
		||||
	Format       string    `json:"format"`
 | 
			
		||||
// AlbumStatsResponse represents the JSON response from the Bunkr API.
 | 
			
		||||
type AlbumStatsResponse struct {
 | 
			
		||||
	Slug      string `json:"slug"`
 | 
			
		||||
	ViewCount int    `json:"viewCount"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Album represents a Bunkkr album.
 | 
			
		||||
| 
						 | 
				
			
			@ -24,3 +28,136 @@ type Album struct {
 | 
			
		|||
	URL    *url.URL `json:"url"`
 | 
			
		||||
	SizeKB int64    `json:"size_kb"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// File represents a single file within a Bunkkr album.
 | 
			
		||||
type File struct {
 | 
			
		||||
	Title        string    `json:"title"`
 | 
			
		||||
	SizeKB       int64     `json:"size_kb"`
 | 
			
		||||
	DateUploaded time.Time `json:"date_uploaded"`
 | 
			
		||||
	URL          *url.URL  `json:"url"`
 | 
			
		||||
	Filename     string    `json:"filename"`
 | 
			
		||||
	Format       string    `json:"format"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Album) GetSize() string {
 | 
			
		||||
	albumSize, err := helper.FormatSizeFromKB(a.SizeKB)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Printf("error: There has been an error parsing the size, defaulting to variable value [%v]", err)
 | 
			
		||||
		albumSize = fmt.Sprintf("Unknown Size (%v Kb)", a.SizeKB)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return albumSize
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *File) GetSize() string {
 | 
			
		||||
	fileSize, err := helper.FormatSizeFromKB(f.SizeKB)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		fmt.Printf("error: There has been an error parsing the size, defaulting to variable value [%v]", err)
 | 
			
		||||
		fileSize = fmt.Sprintf("Unknown Size (%v Kb)", f.SizeKB)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fileSize
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FUNCTIONS
 | 
			
		||||
 | 
			
		||||
func (album *Album) FetchAlbumViews() (int, error) {
 | 
			
		||||
	url := fmt.Sprintf("https://%s/api/album/stats/%s", constants.MAIN_HOST_URL, album.ID)
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequest("GET", url, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, fmt.Errorf("error creating request: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ✅ https://s.bunkr.ru/api/album/stats/p2WBVS3a
 | 
			
		||||
	// 🔴 https://bunkr.cr/api/album/stats/p2WBVS3a
 | 
			
		||||
 | 
			
		||||
	req.Header.Add("Accept-Language", "en-US,en;q=0.6")
 | 
			
		||||
	req.Header.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36")
 | 
			
		||||
 | 
			
		||||
	client := &http.Client{}
 | 
			
		||||
	resp, err := client.Do(req)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, fmt.Errorf("error making request: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer resp.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if resp.StatusCode != http.StatusOK {
 | 
			
		||||
		return -1, fmt.Errorf("request failed with status code: %d", resp.StatusCode)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	body, err := io.ReadAll(resp.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, fmt.Errorf("error reading response body: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var albumStats AlbumStatsResponse
 | 
			
		||||
	err = json.Unmarshal(body, &albumStats)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return -1, fmt.Errorf("error unmarshaling JSON: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return albumStats.ViewCount, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Album) GetKbSize() int64 {
 | 
			
		||||
	var albumSize int64 = 0
 | 
			
		||||
	for _, v := range a.Files {
 | 
			
		||||
		albumSize += v.SizeKB
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return albumSize
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *Album) ToString() string {
 | 
			
		||||
	var builder strings.Builder
 | 
			
		||||
	builder.WriteString("🆔 ID: ")
 | 
			
		||||
	builder.WriteString(a.ID)
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("📂 Album: ")
 | 
			
		||||
	builder.WriteString(a.Title)
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("👁️ Views: ")
 | 
			
		||||
	builder.WriteString(fmt.Sprintf("%d", a.Views))
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("🌐 URL: ")
 | 
			
		||||
	builder.WriteString(a.URL.String())
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("💾 Size: ")
 | 
			
		||||
	builder.WriteString(a.GetSize())
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
 | 
			
		||||
	builder.WriteString("## File List: ##\n")
 | 
			
		||||
	for _, file := range a.Files {
 | 
			
		||||
		fString := strings.ReplaceAll(file.ToString(), "\n", "\n\t")
 | 
			
		||||
 | 
			
		||||
		builder.WriteString("\t")
 | 
			
		||||
		builder.WriteString(fString)
 | 
			
		||||
		builder.WriteString("\n\n")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return builder.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *File) ToString() string {
 | 
			
		||||
	var builder strings.Builder
 | 
			
		||||
	builder.WriteString("📄 Title: ")
 | 
			
		||||
	builder.WriteString(f.Title)
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("💾 Size: ")
 | 
			
		||||
	builder.WriteString(f.GetSize())
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("📅 Uploaded: ")
 | 
			
		||||
	builder.WriteString(f.DateUploaded.Format(time.RFC3339))
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("🌐 URL: ")
 | 
			
		||||
	builder.WriteString(f.URL.String())
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("🔘 filename: ")
 | 
			
		||||
	builder.WriteString(f.Filename)
 | 
			
		||||
	builder.WriteString("\n")
 | 
			
		||||
	builder.WriteString("🟫 format: ")
 | 
			
		||||
	builder.WriteString(f.Format)
 | 
			
		||||
 | 
			
		||||
	return builder.String()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue