feat: Inital commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package driver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.pengzhan.dev/aimaren/internal/crawler"
|
||||
"git.pengzhan.dev/aimaren/internal/notifier"
|
||||
"git.pengzhan.dev/aimaren/internal/storage"
|
||||
)
|
||||
|
||||
const hermesBagsURL = "https://www.hermes.com/us/en/category/women/bags-and-small-leather-goods/bags-and-clutches/"
|
||||
|
||||
// Driver orchestrates the crawling, state management, and notification process.
|
||||
type Driver struct {
|
||||
store storage.Storer
|
||||
notify notifier.Notifier
|
||||
}
|
||||
|
||||
func New(store storage.Storer, notify notifier.Notifier) *Driver {
|
||||
return &Driver{store: store, notify: notify}
|
||||
}
|
||||
|
||||
// Run now uses the consolidated AppState model.
|
||||
func (d *Driver) Run(ctx context.Context) error {
|
||||
log.Println("🚀 Kicking off new crawl cycle...")
|
||||
|
||||
scrapedBags, err := crawler.Scrape(ctx, hermesBagsURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scraping failed: %w", err)
|
||||
}
|
||||
if len(scrapedBags) == 0 {
|
||||
return errors.New("scraper found 0 items, indicating a possible block or site change")
|
||||
}
|
||||
log.Printf("✅ Scraped %d items successfully.", len(scrapedBags))
|
||||
|
||||
// Fetch the entire application state in one call.
|
||||
appState, err := d.store.FetchAppState(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not fetch app state from storage: %w", err)
|
||||
}
|
||||
|
||||
// Process all changes, which modifies the appState object directly.
|
||||
hasChanges, notifications := d.processChanges(appState, scrapedBags)
|
||||
|
||||
// If there are notifications, broadcast to all chat IDs from the app state.
|
||||
if len(notifications) > 0 {
|
||||
if len(appState.ChatIDs) > 0 {
|
||||
log.Printf("✨ Found %d events. Broadcasting to %d subscribers...", len(notifications), len(appState.ChatIDs))
|
||||
for _, msg := range notifications {
|
||||
// The notifier now needs the list of IDs to send to.
|
||||
d.notify.Broadcast(appState.ChatIDs, msg)
|
||||
}
|
||||
} else {
|
||||
log.Println("✨ Found events, but no subscribers to notify.")
|
||||
}
|
||||
}
|
||||
|
||||
// If the state has changed, persist it back to storage.
|
||||
if hasChanges {
|
||||
log.Println("💾 State has changed. Updating storage...")
|
||||
if err := d.store.UpdateAppState(ctx, appState); err != nil {
|
||||
return fmt.Errorf("failed to update app state: %w", err)
|
||||
}
|
||||
log.Println("✅ Storage state updated successfully.")
|
||||
} else {
|
||||
log.Println("✨ No changes detected that require a state update.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processChanges now takes and modifies the AppState directly.
|
||||
func (d *Driver) processChanges(appState *storage.AppState, scrapedBags map[string]crawler.Bag) (bool, []string) {
|
||||
var notifications []string
|
||||
hasChanges := false
|
||||
|
||||
for sku, newBag := range scrapedBags {
|
||||
oldBag, exists := appState.Bags[sku]
|
||||
if !exists || (exists && oldBag.DeleteTimestamp != nil) {
|
||||
hasChanges = true
|
||||
msg := fmt.Sprintf("✨ NEW/RETURNED BAG ✨\n\nName: %s\nAvailable: %t\nURL: %s", newBag.Name, newBag.Availability, newBag.URL)
|
||||
notifications = append(notifications, msg)
|
||||
|
||||
newBag.CreatedTimestamp = time.Now()
|
||||
newBag.UpdatedTimestamp = time.Now()
|
||||
newBag.DeleteTimestamp = nil
|
||||
appState.Bags[sku] = newBag
|
||||
continue
|
||||
}
|
||||
if oldBag.Availability != newBag.Availability {
|
||||
hasChanges = true
|
||||
status := "In Stock!"
|
||||
if !newBag.Availability {
|
||||
status = "Sold Out"
|
||||
}
|
||||
msg := fmt.Sprintf("🚨 STOCK ALERT: %s 🚨\n\nName: %s \nURL: %s", status, newBag.Name, newBag.URL)
|
||||
notifications = append(notifications, msg)
|
||||
|
||||
oldBag.Availability = newBag.Availability
|
||||
oldBag.UpdatedTimestamp = time.Now()
|
||||
appState.Bags[sku] = oldBag
|
||||
}
|
||||
}
|
||||
|
||||
for sku, oldBag := range appState.Bags {
|
||||
if _, exists := scrapedBags[sku]; !exists && oldBag.DeleteTimestamp == nil {
|
||||
hasChanges = true
|
||||
now := time.Now()
|
||||
oldBag.DeleteTimestamp = &now
|
||||
appState.Bags[sku] = oldBag
|
||||
log.Printf("✅ Bag '%s' marked as removed.", oldBag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges, notifications
|
||||
}
|
||||
Reference in New Issue
Block a user