inital commit

This commit is contained in:
David Janowski 2023-05-22 13:08:08 +02:00
commit e0a312bffc
14 changed files with 739 additions and 0 deletions

190
.gitignore vendored Normal file
View File

@ -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/*

8
docker-compose.yml Normal file
View File

@ -0,0 +1,8 @@
version: '2'
services:
neo4j:
image: docker.io/bitnami/neo4j:latest
ports:
- '7474:7474'
- '7473:7473'
- '7687:7687'

25
e621/api/client.go Normal file
View File

@ -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{},
}
}

61
e621/api/favorite.go Normal file
View File

@ -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
}
}

102
e621/api/models/post.go Normal file
View File

@ -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"`
}

83
e621/api/models/user.go Normal file
View File

@ -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"`
}

46
e621/api/users.go Normal file
View File

@ -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
}

14
go.mod Normal file
View File

@ -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
)

16
go.sum Normal file
View File

@ -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=

80
main.go Normal file
View File

@ -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)
}

15
neo4j/connection.go Normal file
View File

@ -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
}

24
neo4j/tag.go Normal file
View File

@ -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
}

27
neo4j/user.go Normal file
View File

@ -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
}

48
utils/config.go Normal file
View File

@ -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
}