commit e0a312bffca12907402d3107ecd8af801bfe05b7 Author: David Janowski Date: Mon May 22 13:08:08 2023 +0200 inital commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18ce7cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,190 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig +# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,goland,go +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,goland,go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +### GoLand ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,goland,go + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + +.idea/* diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..891cf31 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '2' +services: + neo4j: + image: docker.io/bitnami/neo4j:latest + ports: + - '7474:7474' + - '7473:7473' + - '7687:7687' \ No newline at end of file diff --git a/e621/api/client.go b/e621/api/client.go new file mode 100644 index 0000000..c95e7b4 --- /dev/null +++ b/e621/api/client.go @@ -0,0 +1,25 @@ +package api + +import ( + "net/http" +) + +const ( + baseURL = "https://e621.net" +) + +// Client represents the e621 API client. +type Client struct { + apiKey string + username string + client *http.Client +} + +// NewClient creates a new e621 API client. +func NewClient(apiKey string, username string) *Client { + return &Client{ + apiKey: apiKey, + username: username, + client: &http.Client{}, + } +} diff --git a/e621/api/favorite.go b/e621/api/favorite.go new file mode 100644 index 0000000..284ceb0 --- /dev/null +++ b/e621/api/favorite.go @@ -0,0 +1,61 @@ +package api + +import ( + "e621_to_neo4j/e621/api/models" + "fmt" + "io" + "log" + "net/http" + "time" +) + +// GetFavorites retrieves all favorites from the e621 API. +func (c *Client) GetFavorites(user string) ([]models.Post, error) { + time.Sleep(2 * time.Second) + var lastPostID int64 + var allFavorites []models.Post + + for { + url := fmt.Sprintf("%s/posts.json?tags=fav:%s+status:any&page=b%d", baseURL, user, lastPostID) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("User-Agent", "FavGetter (by Selloo)") + req.Header.Add("Accept", "application/json") + req.SetBasicAuth(c.username, c.apiKey) + + resp, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to retrieve posts: %s", resp.Status) + } + + fetchedFavorites, err := models.UnmarshalE621Post(body) + if err != nil { + log.Printf(err.Error()) + } + // Append the fetched posts to the result slice + allFavorites = append(allFavorites, fetchedFavorites.Posts...) + + // If no more posts are returned, return the accumulated favorites + if len(fetchedFavorites.Posts) == 0 { + return allFavorites, nil + } + + // Update the last post ID for the next page request + lastPostID = fetchedFavorites.Posts[len(fetchedFavorites.Posts)-1].ID + + } + +} diff --git a/e621/api/models/post.go b/e621/api/models/post.go new file mode 100644 index 0000000..df71e48 --- /dev/null +++ b/e621/api/models/post.go @@ -0,0 +1,102 @@ +package models + +import "encoding/json" + +func UnmarshalE621Post(data []byte) (E621Post, error) { + var r E621Post + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *E621Post) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +type E621Post struct { + Posts []Post `json:"posts"` +} + +type Post struct { + ID int64 `json:"id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + File File `json:"file"` + Preview Preview `json:"preview"` + Sample Sample `json:"sample"` + Score Score `json:"score"` + Tags Tags `json:"tags"` + LockedTags []interface{} `json:"locked_tags"` + ChangeSeq int64 `json:"change_seq"` + Flags Flags `json:"flags"` + Rating string `json:"rating"` + FavCount int64 `json:"fav_count"` + Sources []string `json:"sources"` + Pools []interface{} `json:"pools"` + Relationships Relationships `json:"relationships"` + ApproverID *int64 `json:"approver_id"` + UploaderID int64 `json:"uploader_id"` + Description string `json:"description"` + CommentCount int64 `json:"comment_count"` + IsFavorited bool `json:"is_favorited"` + HasNotes bool `json:"has_notes"` + Duration interface{} `json:"duration"` +} + +type File struct { + Width int64 `json:"width"` + Height int64 `json:"height"` + EXT string `json:"ext"` + Size int64 `json:"size"` + Md5 string `json:"md5"` + URL string `json:"url"` +} + +type Flags struct { + Pending bool `json:"pending"` + Flagged bool `json:"flagged"` + NoteLocked bool `json:"note_locked"` + StatusLocked bool `json:"status_locked"` + RatingLocked bool `json:"rating_locked"` + Deleted bool `json:"deleted"` +} + +type Preview struct { + Width int64 `json:"width"` + Height int64 `json:"height"` + URL string `json:"url"` +} + +type Relationships struct { + ParentID interface{} `json:"parent_id"` + HasChildren bool `json:"has_children"` + HasActiveChildren bool `json:"has_active_children"` + Children []interface{} `json:"children"` +} + +type Sample struct { + Has bool `json:"has"` + Height int64 `json:"height"` + Width int64 `json:"width"` + URL string `json:"url"` + Alternates Alternates `json:"alternates"` +} + +type Alternates struct { +} + +type Score struct { + Up int64 `json:"up"` + Down int64 `json:"down"` + Total int64 `json:"total"` +} + +type Tags struct { + General []string `json:"general"` + Species []string `json:"species"` + Character []string `json:"character"` + Copyright []string `json:"copyright"` + Artist []string `json:"artist"` + Invalid []interface{} `json:"invalid"` + Lore []interface{} `json:"lore"` + Meta []string `json:"meta"` +} diff --git a/e621/api/models/user.go b/e621/api/models/user.go new file mode 100644 index 0000000..7018463 --- /dev/null +++ b/e621/api/models/user.go @@ -0,0 +1,83 @@ +package models + +import "encoding/json" + +func UnmarshalE621User(data []byte) (E621User, error) { + var r E621User + err := json.Unmarshal(data, &r) + return r, err +} + +func (r *E621User) Marshal() ([]byte, error) { + return json.Marshal(r) +} + +type E621User struct { + WikiPageVersionCount int64 `json:"wiki_page_version_count"` + ArtistVersionCount int64 `json:"artist_version_count"` + PoolVersionCount int64 `json:"pool_version_count"` + ForumPostCount int64 `json:"forum_post_count"` + CommentCount int64 `json:"comment_count"` + FlagCount int64 `json:"flag_count"` + FavoriteCount int64 `json:"favorite_count"` + PositiveFeedbackCount int64 `json:"positive_feedback_count"` + NeutralFeedbackCount int64 `json:"neutral_feedback_count"` + NegativeFeedbackCount int64 `json:"negative_feedback_count"` + UploadLimit int64 `json:"upload_limit"` + ID int64 `json:"id"` + CreatedAt string `json:"created_at"` + Name string `json:"name"` + Level int64 `json:"level"` + BaseUploadLimit int64 `json:"base_upload_limit"` + PostUploadCount int64 `json:"post_upload_count"` + PostUpdateCount int64 `json:"post_update_count"` + NoteUpdateCount int64 `json:"note_update_count"` + IsBanned bool `json:"is_banned"` + CanApprovePosts bool `json:"can_approve_posts"` + CanUploadFree bool `json:"can_upload_free"` + LevelString string `json:"level_string"` + AvatarID int64 `json:"avatar_id"` + ShowAvatars bool `json:"show_avatars"` + BlacklistAvatars bool `json:"blacklist_avatars"` + BlacklistUsers bool `json:"blacklist_users"` + DescriptionCollapsedInitially bool `json:"description_collapsed_initially"` + HideComments bool `json:"hide_comments"` + ShowHiddenComments bool `json:"show_hidden_comments"` + ShowPostStatistics bool `json:"show_post_statistics"` + HasMail bool `json:"has_mail"` + ReceiveEmailNotifications bool `json:"receive_email_notifications"` + EnableKeyboardNavigation bool `json:"enable_keyboard_navigation"` + EnablePrivacyMode bool `json:"enable_privacy_mode"` + StyleUsernames bool `json:"style_usernames"` + EnableAutoComplete bool `json:"enable_auto_complete"` + HasSavedSearches bool `json:"has_saved_searches"` + DisableCroppedThumbnails bool `json:"disable_cropped_thumbnails"` + DisableMobileGestures bool `json:"disable_mobile_gestures"` + EnableSafeMode bool `json:"enable_safe_mode"` + DisableResponsiveMode bool `json:"disable_responsive_mode"` + DisablePostTooltips bool `json:"disable_post_tooltips"` + NoFlagging bool `json:"no_flagging"` + NoFeedback bool `json:"no_feedback"` + DisableUserDmails bool `json:"disable_user_dmails"` + EnableCompactUploader bool `json:"enable_compact_uploader"` + ReplacementsBeta bool `json:"replacements_beta"` + IsBdStaff bool `json:"is_bd_staff"` + UpdatedAt string `json:"updated_at"` + Email string `json:"email"` + LastLoggedInAt string `json:"last_logged_in_at"` + LastForumReadAt string `json:"last_forum_read_at"` + RecentTags string `json:"recent_tags"` + CommentThreshold int64 `json:"comment_threshold"` + DefaultImageSize string `json:"default_image_size"` + FavoriteTags string `json:"favorite_tags"` + BlacklistedTags string `json:"blacklisted_tags"` + TimeZone string `json:"time_zone"` + PerPage int64 `json:"per_page"` + CustomStyle string `json:"custom_style"` + APIRegenMultiplier int64 `json:"api_regen_multiplier"` + APIBurstLimit int64 `json:"api_burst_limit"` + RemainingAPILimit int64 `json:"remaining_api_limit"` + StatementTimeout int64 `json:"statement_timeout"` + FavoriteLimit int64 `json:"favorite_limit"` + TagQueryLimit int64 `json:"tag_query_limit"` +} diff --git a/e621/api/users.go b/e621/api/users.go new file mode 100644 index 0000000..4416234 --- /dev/null +++ b/e621/api/users.go @@ -0,0 +1,46 @@ +package api + +import ( + "e621_to_neo4j/e621/api/models" + "fmt" + "io" + "net/http" + "time" +) + +// GetUserInfo retrieves the users information from e621 API. +func (c *Client) GetUserInfo(user string) (models.E621User, error) { + var e621User models.E621User + time.Sleep(2 * time.Second) + url := fmt.Sprintf("%s/users/%s.json", baseURL, user) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return e621User, err + } + + req.Header.Set("User-Agent", "FavGetter (by Selloo)") + req.Header.Add("Accept", "application/json") + req.SetBasicAuth(c.username, c.apiKey) + + resp, err := c.client.Do(req) + if err != nil { + return e621User, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return e621User, err + } + + if resp.StatusCode != http.StatusOK { + return e621User, fmt.Errorf("failed to retrieve posts: %s", resp.Status) + } + + e621User, err = models.UnmarshalE621User(body) + if err != nil { + return e621User, err + } + + return e621User, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5ce0f67 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module e621_to_neo4j + +go 1.20 + +require ( + github.com/caarlos0/env v3.5.0+incompatible + github.com/neo4j/neo4j-go-driver/v5 v5.8.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/stretchr/testify v1.5.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7312b1e --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs= +github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y= +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/neo4j/neo4j-go-driver/v5 v5.8.1 h1:IysKg6KJIUgyItmnHRRrt2N8srbd6znMslRW3qQErTQ= +github.com/neo4j/neo4j-go-driver/v5 v5.8.1/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5608b66 --- /dev/null +++ b/main.go @@ -0,0 +1,80 @@ +package main + +import ( + "context" + "e621_to_neo4j/e621/api" + "e621_to_neo4j/neo4j" + "e621_to_neo4j/utils" + "log" + "time" +) + +func main() { + config, err := utils.LoadConfig() + if err != nil { + log.Println(err) + } + + driver, err := neo4j.NewConnection(config.Neo4jURL, config.Neo4jUsername, config.Neo4jPassword) + if err != nil { + log.Fatal(err) + } + + ctx := context.Background() + defer driver.Close(ctx) + + e621Client := api.NewClient(config.E621APIKey, config.E621Username) + user, err := e621Client.GetUserInfo("selloo") + if err != nil { + log.Fatal(err) + } + + err = neo4j.CreateUserNode(ctx, driver, user) + if err != nil { + log.Fatal(err) + } + + favs, err := e621Client.GetFavorites(user.Name) + if err != nil { + log.Fatal(err) + } + start := time.Now() + for i, fav := range favs { + + log.Printf("The e621 post with the id %d has %d general Tags, %d character Tags, %d copyright Tags, %d artist Tags.", fav.ID, len(fav.Tags.General), len(fav.Tags.Character), len(fav.Tags.Copyright), len(fav.Tags.Artist)) + log.Printf("Uploaded Posts: %d", i) + + for _, general := range fav.Tags.General { + log.Printf("TagType: General - Tag: %s", general) + err := neo4j.CreateTagNode(ctx, driver, general, "general") + if err != nil { + log.Fatal(err) + } + } + for _, character := range fav.Tags.Character { + log.Printf("TagType: Character - Tag: %s", character) + err := neo4j.CreateTagNode(ctx, driver, character, "character") + if err != nil { + log.Fatal(err) + } + } + + for _, copyright := range fav.Tags.Copyright { + log.Printf("TagType: Copyright - Tag: %s", copyright) + err := neo4j.CreateTagNode(ctx, driver, copyright, "copyright") + if err != nil { + log.Fatal(err) + } + } + + for _, artist := range fav.Tags.Artist { + log.Printf("TagType: Artist - Tag: %s", artist) + err := neo4j.CreateTagNode(ctx, driver, artist, "artist") + if err != nil { + log.Fatal(err) + } + } + } + elapsed := time.Since(start) + log.Printf("This took %s", elapsed) +} diff --git a/neo4j/connection.go b/neo4j/connection.go new file mode 100644 index 0000000..de0f2fb --- /dev/null +++ b/neo4j/connection.go @@ -0,0 +1,15 @@ +package neo4j + +import ( + "fmt" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func NewConnection(uri string, username string, password string) (neo4j.DriverWithContext, error) { + driver, err := neo4j.NewDriverWithContext(uri, neo4j.BasicAuth(username, password, "")) + if err != nil { + return nil, fmt.Errorf("failed to create Neo4j driver: %v", err) + } + return driver, nil + +} diff --git a/neo4j/tag.go b/neo4j/tag.go new file mode 100644 index 0000000..61c221c --- /dev/null +++ b/neo4j/tag.go @@ -0,0 +1,24 @@ +package neo4j + +import ( + "context" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" +) + +func CreateTagNode(ctx context.Context, driver neo4j.DriverWithContext, name string, tagType string) error { + query := ` + MERGE (u:e621Tag {e621Tag: $name, e621TagType: $tagType}) + RETURN u +` + params := map[string]interface{}{ + "name": name, + "tagType": tagType, + } + + _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) + if err != nil { + return err + } + + return nil +} diff --git a/neo4j/user.go b/neo4j/user.go new file mode 100644 index 0000000..e0c2b75 --- /dev/null +++ b/neo4j/user.go @@ -0,0 +1,27 @@ +package neo4j + +import ( + "context" + "e621_to_neo4j/e621/api/models" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "log" +) + +func CreateUserNode(ctx context.Context, driver neo4j.DriverWithContext, user models.E621User) error { + query := ` + MERGE (u:e621User {e621ID: $id, e621Username: $name}) + RETURN u + ` + params := map[string]interface{}{ + "id": user.ID, + "name": user.Name, + } + + _, err := neo4j.ExecuteQuery(ctx, driver, query, params, neo4j.EagerResultTransformer) + if err != nil { + return err + } + + log.Println("User node created successfully!") + return nil +} diff --git a/utils/config.go b/utils/config.go new file mode 100644 index 0000000..e83cecf --- /dev/null +++ b/utils/config.go @@ -0,0 +1,48 @@ +package utils + +import ( + "fmt" + "github.com/caarlos0/env" +) + +type Config struct { + E621APIKey string `env:"e621_API_KEY"` + E621Username string `env:"e621_USERNAME"` + Neo4jURL string `env:"NEO4J_URL"` + Neo4jUsername string `env:"NEO4J_USERNAME"` + Neo4jPassword string `env:"NEO4J_PASSWORD"` +} + +// LoadConfig loads the configuration from environment variables +func LoadConfig() (*Config, error) { + config := &Config{} + if err := env.Parse(config); err != nil { + return nil, fmt.Errorf("error parsing configuration: %w", err) + } + + if err := ValidateConfig(config); err != nil { + return nil, fmt.Errorf("configuration validation failed: %w", err) + } + + return config, nil +} + +// ValidateConfig checks if all required fields in the configuration are set +func ValidateConfig(config *Config) error { + if config.E621APIKey == "" { + return fmt.Errorf("e621_API_KEY is not set") + } + if config.E621Username == "" { + return fmt.Errorf("e621_USERNAME is not set") + } + if config.Neo4jURL == "" { + return fmt.Errorf("NEO4J_URL is not set") + } + if config.Neo4jUsername == "" { + return fmt.Errorf("NEO4J_USERNAME is not set") + } + if config.Neo4jPassword == "" { + return fmt.Errorf("NEO4J_PASSWORD is not set") + } + return nil +}