Created
November 14, 2025 15:43
-
-
Save Xetera/645481c34a3d054c4231ccb516e51e6e to your computer and use it in GitHub Desktop.
Gyazo's PRO data export does NOT properly include the created at field for the images you download. This script restores that data back into the images given they're all in the same folder and that you have an api key.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Yes this is vibe coded. It worked fine for me | |
| package main | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "time" | |
| ) | |
| // GyazoImage represents an image object from the API | |
| type GyazoImage struct { | |
| ImageID string `json:"image_id"` | |
| CreatedAt string `json:"created_at"` | |
| } | |
| // Checkpoint represents saved progress | |
| type Checkpoint struct { | |
| Page int `json:"page"` | |
| TotalProcessed int `json:"total_processed"` | |
| TotalUpdated int `json:"total_updated"` | |
| Timestamp time.Time `json:"timestamp"` | |
| } | |
| const checkpointFile = ".gyazo_checkpoint.json" | |
| // getGyazoImages fetches images from Gyazo API for a given page | |
| func getGyazoImages(accessToken string, page int) ([]GyazoImage, error) { | |
| url := fmt.Sprintf("https://api.gyazo.com/api/images?access_token=%s&page=%d", accessToken, page) | |
| resp, err := http.Get(url) | |
| if err != nil { | |
| return nil, fmt.Errorf("error fetching page %d: %w", page, err) | |
| } | |
| defer resp.Body.Close() | |
| if resp.StatusCode != http.StatusOK { | |
| return nil, fmt.Errorf("API returned status %d for page %d", resp.StatusCode, page) | |
| } | |
| body, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| return nil, fmt.Errorf("error reading response: %w", err) | |
| } | |
| var images []GyazoImage | |
| if err := json.Unmarshal(body, &images); err != nil { | |
| return nil, fmt.Errorf("error parsing JSON: %w", err) | |
| } | |
| return images, nil | |
| } | |
| // findFileByImageID finds a file matching the image_id (without extension) | |
| func findFileByImageID(imageID, searchDir string) (string, error) { | |
| pattern := filepath.Join(searchDir, imageID+".*") | |
| matches, err := filepath.Glob(pattern) | |
| if err != nil { | |
| return "", err | |
| } | |
| if len(matches) > 0 { | |
| return matches[0], nil | |
| } | |
| return "", nil | |
| } | |
| // updateFileTimestamp updates the access and modification time of a file | |
| func updateFileTimestamp(filepath, isoTimestamp string) error { | |
| // Try multiple timestamp formats | |
| formats := []string{ | |
| time.RFC3339, // 2006-01-02T15:04:05Z07:00 | |
| "2006-01-02T15:04:05.999Z07:00", // With milliseconds | |
| "2006-01-02T15:04:05-0700", // Without colon in timezone (e.g., +0000) | |
| "2006-01-02T15:04:05.999-0700", // With milliseconds, no colon in timezone | |
| } | |
| var t time.Time | |
| var err error | |
| for _, format := range formats { | |
| t, err = time.Parse(format, isoTimestamp) | |
| if err == nil { | |
| break | |
| } | |
| } | |
| if err != nil { | |
| return fmt.Errorf("error parsing timestamp '%s': %w", isoTimestamp, err) | |
| } | |
| // Update both access time and modification time | |
| if err := os.Chtimes(filepath, t, t); err != nil { | |
| return fmt.Errorf("error updating timestamps: %w", err) | |
| } | |
| return nil | |
| } | |
| // saveCheckpoint saves the current progress to a checkpoint file | |
| func saveCheckpoint(page, totalProcessed, totalUpdated int) error { | |
| checkpoint := Checkpoint{ | |
| Page: page, | |
| TotalProcessed: totalProcessed, | |
| TotalUpdated: totalUpdated, | |
| Timestamp: time.Now(), | |
| } | |
| data, err := json.MarshalIndent(checkpoint, "", " ") | |
| if err != nil { | |
| return err | |
| } | |
| return os.WriteFile(checkpointFile, data, 0644) | |
| } | |
| // loadCheckpoint loads progress from a checkpoint file | |
| func loadCheckpoint() (*Checkpoint, error) { | |
| data, err := os.ReadFile(checkpointFile) | |
| if err != nil { | |
| if os.IsNotExist(err) { | |
| return nil, nil | |
| } | |
| return nil, err | |
| } | |
| var checkpoint Checkpoint | |
| if err := json.Unmarshal(data, &checkpoint); err != nil { | |
| return nil, err | |
| } | |
| return &checkpoint, nil | |
| } | |
| // promptUser prompts the user for input | |
| func promptUser(prompt string) string { | |
| fmt.Print(prompt) | |
| var input string | |
| fmt.Scanln(&input) | |
| return strings.TrimSpace(input) | |
| } | |
| func main() { | |
| // Get access token from user | |
| fmt.Print("Enter your Gyazo API access token: ") | |
| var accessToken string | |
| fmt.Scanln(&accessToken) | |
| accessToken = strings.TrimSpace(accessToken) | |
| if accessToken == "" { | |
| fmt.Println("Error: Access token is required") | |
| return | |
| } | |
| // Optional: specify a different search directory | |
| fmt.Print("Enter directory to search for files (press Enter for current directory): ") | |
| var searchDir string | |
| fmt.Scanln(&searchDir) | |
| searchDir = strings.TrimSpace(searchDir) | |
| if searchDir == "" { | |
| searchDir = "." | |
| } | |
| absPath, err := filepath.Abs(searchDir) | |
| if err != nil { | |
| fmt.Printf("Error resolving directory path: %v\n", err) | |
| return | |
| } | |
| fmt.Printf("\nSearching for files in: %s\n\n", absPath) | |
| // Check for existing checkpoint | |
| checkpoint, err := loadCheckpoint() | |
| if err != nil { | |
| fmt.Printf("Warning: Could not load checkpoint: %v\n", err) | |
| } | |
| page := 1 | |
| totalProcessed := 0 | |
| totalUpdated := 0 | |
| if checkpoint != nil { | |
| fmt.Printf("Found checkpoint from %s\n", checkpoint.Timestamp.Format(time.RFC3339)) | |
| fmt.Printf(" Last processed page: %d\n", checkpoint.Page) | |
| fmt.Printf(" Total processed: %d\n", checkpoint.TotalProcessed) | |
| fmt.Printf(" Total updated: %d\n", checkpoint.TotalUpdated) | |
| resume := promptUser("\nResume from checkpoint? (y/n): ") | |
| if strings.ToLower(resume) == "y" { | |
| page = checkpoint.Page | |
| totalProcessed = checkpoint.TotalProcessed | |
| totalUpdated = checkpoint.TotalUpdated | |
| fmt.Printf("\nResuming from page %d...\n\n", page) | |
| } else { | |
| fmt.Println("\nStarting fresh...\n") | |
| } | |
| } | |
| // Process pages | |
| for { | |
| fmt.Printf("Fetching page %d...\n", page) | |
| images, err := getGyazoImages(accessToken, page) | |
| if err != nil { | |
| fmt.Printf("Error: %v\n", err) | |
| break | |
| } | |
| // Stop if no images returned or empty array | |
| if len(images) == 0 { | |
| fmt.Printf("No more images found at page %d\n", page) | |
| break | |
| } | |
| fmt.Printf("Processing %d images from page %d...\n", len(images), page) | |
| for _, image := range images { | |
| if image.ImageID == "" || image.CreatedAt == "" { | |
| fmt.Printf("Skipping image with missing data: %+v\n", image) | |
| continue | |
| } | |
| totalProcessed++ | |
| // Find matching file | |
| filepath, err := findFileByImageID(image.ImageID, searchDir) | |
| if err != nil { | |
| fmt.Printf("Error searching for file %s: %v\n", image.ImageID, err) | |
| continue | |
| } | |
| if filepath != "" { | |
| if err := updateFileTimestamp(filepath, image.CreatedAt); err != nil { | |
| fmt.Printf("✗ Failed to update: %s (%v)\n", filepath, err) | |
| } else { | |
| fmt.Printf("✓ Updated: %s -> %s\n", filepath, image.CreatedAt) | |
| totalUpdated++ | |
| } | |
| } else { | |
| fmt.Printf(" File not found for image_id: %s\n", image.ImageID) | |
| } | |
| } | |
| // Save checkpoint after processing each page | |
| page++ | |
| if err := saveCheckpoint(page, totalProcessed, totalUpdated); err != nil { | |
| fmt.Printf("Warning: Could not save checkpoint: %v\n", err) | |
| } | |
| // Small delay to be nice to the API | |
| time.Sleep(500 * time.Millisecond) | |
| } | |
| // Delete checkpoint file when complete | |
| if err := os.Remove(checkpointFile); err != nil && !os.IsNotExist(err) { | |
| fmt.Printf("Warning: Could not remove checkpoint file: %v\n", err) | |
| } else { | |
| fmt.Println("\nCheckpoint file removed (processing complete)") | |
| } | |
| fmt.Println("\n" + strings.Repeat("=", 50)) | |
| fmt.Println("Summary:") | |
| fmt.Printf(" Total images processed: %d\n", totalProcessed) | |
| fmt.Printf(" Files updated: %d\n", totalUpdated) | |
| fmt.Printf(" Files not found: %d\n", totalProcessed-totalUpdated) | |
| fmt.Println(strings.Repeat("=", 50)) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment