Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.7
version: v2.10.1
only-new-issues: true
14 changes: 14 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,28 @@ linters:
# Style and formatting
- asciicheck
- bidichk
- canonicalheader
- containedctx
- contextcheck
- dupword
- durationcheck
- errname
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- gocheckcompilerdirectives
- godot
- iface
- importas
- intrange
- mirror
- modernize
- noinlineerr
- perfsprint
- tagalign
- usetesting
exclusions:
rules:
- text: "exported: exported"
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
-include .env

BINARY_NAME=anilist-mal-sync
LINT_VERSION=v2.2.2
LINT_VERSION=v2.10.1
DOCKER_LINT_CMD=docker run --rm -v $(PWD):/app -w /app golangci/golangci-lint:$(LINT_VERSION)

.DEFAULT_GOAL := help
Expand Down
39 changes: 20 additions & 19 deletions anilist.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"

Expand Down Expand Up @@ -97,7 +98,7 @@ func (c *AnilistClient) GetUserMangaList(ctx context.Context) ([]verniy.MediaLis
return mediaListGroups, nil
}

// newAnilistOAuth creates AniList OAuth with optional initialization
// newAnilistOAuth creates AniList OAuth with optional initialization.
func newAnilistOAuth(ctx context.Context, config Config, initWithToken bool) (*OAuth, error) {
verifier := oauth2.GenerateVerifier()

Expand Down Expand Up @@ -129,7 +130,7 @@ func NewAnilistOAuthWithoutInit(ctx context.Context, config Config) (*OAuth, err
return newAnilistOAuth(ctx, config, false)
}

// GraphQLError represents a GraphQL error
// GraphQLError represents a GraphQL error.
type GraphQLError struct {
Message string `json:"message"`
Status int `json:"status"`
Expand All @@ -139,13 +140,13 @@ type GraphQLError struct {
} `json:"locations"`
}

// GraphQLResponse represents a GraphQL response with potential errors
// GraphQLResponse represents a GraphQL response with potential errors.
type GraphQLResponse struct {
Data json.RawMessage `json:"data"`
Errors []GraphQLError `json:"errors"`
}

// SaveMediaListEntry represents the response from AniList SaveMediaListEntry mutation
// SaveMediaListEntry represents the response from AniList SaveMediaListEntry mutation.
type SaveMediaListEntry struct {
Data struct {
SaveMediaListEntry struct {
Expand Down Expand Up @@ -201,7 +202,7 @@ func (c *AnilistClient) UpdateAnimeEntry(
}
`

variables := map[string]interface{}{
variables := map[string]any{
"mediaId": mediaID,
"status": status,
"progress": progress,
Expand All @@ -215,7 +216,7 @@ func (c *AnilistClient) UpdateAnimeEntry(
variables["completedAt"] = fd
}

requestBody := map[string]interface{}{
requestBody := map[string]any{
"query": mutation,
"variables": variables,
}
Expand Down Expand Up @@ -300,7 +301,7 @@ func (c *AnilistClient) UpdateMangaEntry(
}
`

variables := map[string]interface{}{
variables := map[string]any{
"mediaId": mediaID,
"status": status,
"progress": progress,
Expand All @@ -315,7 +316,7 @@ func (c *AnilistClient) UpdateMangaEntry(
variables["completedAt"] = fd
}

requestBody := map[string]interface{}{
requestBody := map[string]any{
"query": mutation,
"variables": variables,
}
Expand Down Expand Up @@ -365,10 +366,10 @@ func (c *AnilistClient) ToggleFavourite(ctx context.Context, animeID, mangaID in
defer cancel()

if animeID <= 0 && mangaID <= 0 {
return fmt.Errorf("at least one of animeID or mangaID must be positive")
return errors.New("at least one of animeID or mangaID must be positive")
}
if animeID > 0 && mangaID > 0 {
return fmt.Errorf("only one of animeID or mangaID can be specified per call")
return errors.New("only one of animeID or mangaID can be specified per call")
}

mutation := `
Expand Down Expand Up @@ -420,7 +421,7 @@ func (c *AnilistClient) ToggleFavourite(ctx context.Context, animeID, mangaID in
return nil
}

// GetAnimeByID gets an anime from AniList by ID
// GetAnimeByID gets an anime from AniList by ID.
func (c *AnilistClient) GetAnimeByID(ctx context.Context, id int) (*verniy.Media, error) {
ctx, cancel := withTimeout(ctx, c.httpTimeout)
defer cancel()
Expand All @@ -442,7 +443,7 @@ func (c *AnilistClient) GetAnimeByID(ctx context.Context, id int) (*verniy.Media
return media, nil
}

// GetAnimesByName searches for anime on AniList by name
// GetAnimesByName searches for anime on AniList by name.
func (c *AnilistClient) GetAnimesByName(ctx context.Context, name string) ([]verniy.Media, error) {
ctx, cancel := withTimeout(ctx, c.httpTimeout)
defer cancel()
Expand All @@ -464,7 +465,7 @@ func (c *AnilistClient) GetAnimesByName(ctx context.Context, name string) ([]ver
return page.Media, nil
}

// GetAnimeByMALID gets an anime from AniList by MAL ID
// GetAnimeByMALID gets an anime from AniList by MAL ID.
func (c *AnilistClient) GetAnimeByMALID(ctx context.Context, malID int) (*verniy.Media, error) {
ctx, cancel := withTimeout(ctx, c.httpTimeout)
defer cancel()
Expand All @@ -489,7 +490,7 @@ func (c *AnilistClient) GetAnimeByMALID(ctx context.Context, malID int) (*verniy
return &page.Media[0], nil
}

// GetMangaByID gets a manga from AniList by ID
// GetMangaByID gets a manga from AniList by ID.
func (c *AnilistClient) GetMangaByID(ctx context.Context, id int) (*verniy.Media, error) {
ctx, cancel := withTimeout(ctx, c.httpTimeout)
defer cancel()
Expand All @@ -513,7 +514,7 @@ func (c *AnilistClient) GetMangaByID(ctx context.Context, id int) (*verniy.Media
return media, nil
}

// GetMangasByName searches for manga on AniList by name
// GetMangasByName searches for manga on AniList by name.
func (c *AnilistClient) GetMangasByName(ctx context.Context, name string) ([]verniy.Media, error) {
ctx, cancel := withTimeout(ctx, c.httpTimeout)
defer cancel()
Expand All @@ -537,7 +538,7 @@ func (c *AnilistClient) GetMangasByName(ctx context.Context, name string) ([]ver
return page.Media, nil
}

// GetMangaByMALID gets a manga from AniList by MAL ID
// GetMangaByMALID gets a manga from AniList by MAL ID.
func (c *AnilistClient) GetMangaByMALID(ctx context.Context, malID int) (*verniy.Media, error) {
ctx, cancel := withTimeout(ctx, c.httpTimeout)
defer cancel()
Expand Down Expand Up @@ -583,7 +584,7 @@ func timeToFuzzyDateInput(t *time.Time) map[string]int {
}
}

// GetUserScoreFormat retrieves the user's score format preference from AniList
// GetUserScoreFormat retrieves the user's score format preference from AniList.
func (c *AnilistClient) GetUserScoreFormat(ctx context.Context) (verniy.ScoreFormat, error) {
ctx, cancel := withTimeout(ctx, c.httpTimeout)
defer cancel()
Expand All @@ -597,11 +598,11 @@ func (c *AnilistClient) GetUserScoreFormat(ctx context.Context) (verniy.ScoreFor
}

if user.MediaListOptions == nil {
return "", fmt.Errorf("user media list options is nil")
return "", errors.New("user media list options is nil")
}

if user.MediaListOptions.ScoreFormat == nil {
return "", fmt.Errorf("user score format is nil")
return "", errors.New("user score format is nil")
}

return *user.MediaListOptions.ScoreFormat, nil
Expand Down
8 changes: 4 additions & 4 deletions anime.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,12 @@ func (a Anime) GetTargetID() TargetID {
return TargetID(a.IDMal)
}

// GetAniListID returns the AniList ID
// GetAniListID returns the AniList ID.
func (a Anime) GetAniListID() TargetID {
return TargetID(a.IDAnilist)
}

// GetMALID returns the MAL ID
// GetMALID returns the MAL ID.
func (a Anime) GetMALID() TargetID {
return TargetID(a.IDMal)
}
Expand Down Expand Up @@ -225,7 +225,7 @@ func (a Anime) SameTitleWithTarget(ctx context.Context, t Target) bool {
}

// IsPotentiallyIncorrectMatch checks if a match might be incorrect
// Returns true if the match should be rejected
// Returns true if the match should be rejected.
func (a Anime) IsPotentiallyIncorrectMatch(t Target) bool {
b, ok := t.(Anime)
if !ok {
Expand Down Expand Up @@ -257,7 +257,7 @@ func (a Anime) IsPotentiallyIncorrectMatch(t Target) bool {
return false
}

// IdenticalTitleMatch checks if titles are truly identical (not just similar)
// IdenticalTitleMatch checks if titles are truly identical (not just similar).
func (a Anime) IdenticalTitleMatch(b Anime) bool {
// Exact match on any title field
if a.TitleEN != "" && a.TitleEN == b.TitleEN {
Expand Down
39 changes: 24 additions & 15 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,34 +371,39 @@ func (a *App) saveUnmappedState(ctx context.Context, updaters []*Updater) {
Entries: allUnmapped,
UpdatedAt: time.Now(),
}
if saveErr := state.Save(""); saveErr != nil {
saveErr := state.Save("")
if saveErr != nil {
LogWarn(ctx, "Failed to save unmapped state: %v", saveErr)
}
}

func (a *App) runNormalSync(ctx context.Context) error {
if a.mangaSync || a.allSync {
if err := a.syncManga(ctx); err != nil {
err := a.syncManga(ctx)
if err != nil {
return fmt.Errorf("error syncing manga: %w", err)
}
}

if !(a.mangaSync) || a.allSync {
if err := a.syncAnime(ctx); err != nil {
err := a.syncAnime(ctx)
if err != nil {
return fmt.Errorf("error syncing anime: %w", err)
}
}

// Save Hato cache if enabled
if a.hatoClient != nil {
if err := a.hatoClient.SaveCache(ctx); err != nil {
err := a.hatoClient.SaveCache(ctx)
if err != nil {
LogWarn(ctx, "Failed to save Hato cache: %v", err)
}
}

// Save Jikan cache if enabled
if a.jikanClient != nil {
if err := a.jikanClient.SaveCache(ctx); err != nil {
err := a.jikanClient.SaveCache(ctx)
if err != nil {
LogWarn(ctx, "Failed to save Jikan cache: %v", err)
}
}
Expand All @@ -408,55 +413,59 @@ func (a *App) runNormalSync(ctx context.Context) error {

func (a *App) runReverseSync(ctx context.Context) error {
if a.mangaSync || a.allSync {
if err := a.reverseSyncManga(ctx); err != nil {
err := a.reverseSyncManga(ctx)
if err != nil {
return fmt.Errorf("error reverse syncing manga: %w", err)
}
}

if !(a.mangaSync) || a.allSync {
if err := a.reverseSyncAnime(ctx); err != nil {
err := a.reverseSyncAnime(ctx)
if err != nil {
return fmt.Errorf("error reverse syncing anime: %w", err)
}
}

// Save Hato cache if enabled
if a.hatoClient != nil {
if err := a.hatoClient.SaveCache(ctx); err != nil {
err := a.hatoClient.SaveCache(ctx)
if err != nil {
LogWarn(ctx, "Failed to save Hato cache: %v", err)
}
}

// Save Jikan cache if enabled
if a.jikanClient != nil {
if err := a.jikanClient.SaveCache(ctx); err != nil {
err := a.jikanClient.SaveCache(ctx)
if err != nil {
LogWarn(ctx, "Failed to save Jikan cache: %v", err)
}
}

return nil
}

// syncAnimeFromAnilistToMAL syncs anime from AniList to MAL
// syncAnimeFromAnilistToMAL syncs anime from AniList to MAL.
func (a *App) syncAnime(ctx context.Context) error {
return a.performSync(ctx, mediaTypeAnime, false, a.animeUpdater)
}

// syncMangaFromAnilistToMAL syncs manga from AniList to MAL
// syncMangaFromAnilistToMAL syncs manga from AniList to MAL.
func (a *App) syncManga(ctx context.Context) error {
return a.performSync(ctx, mediaTypeManga, false, a.mangaUpdater)
}

// reverseSyncAnimeFromMALToAnilist syncs anime from MAL to AniList
// reverseSyncAnimeFromMALToAnilist syncs anime from MAL to AniList.
func (a *App) reverseSyncAnime(ctx context.Context) error {
return a.performSync(ctx, "anime", true, a.reverseAnimeUpdater)
}

// reverseSyncMangaFromMALToAnilist syncs manga from MAL to AniList
// reverseSyncMangaFromMALToAnilist syncs manga from MAL to AniList.
func (a *App) reverseSyncManga(ctx context.Context) error {
return a.performSync(ctx, "manga", true, a.reverseMangaUpdater)
}

// performSync is a generic sync function that handles both anime and manga syncing
// performSync is a generic sync function that handles both anime and manga syncing.
func (a *App) performSync(ctx context.Context, mediaType string, reverse bool, updater *Updater) error {
var srcs []Source
var tgts []Target
Expand Down Expand Up @@ -605,7 +614,7 @@ func (a *App) fetchFromMALToAnilist(ctx context.Context, mediaType string, prefi
// For reverse sync (MAL → AniList), we need:
// - MAL ID (from MAL list)
// - AniList ID (from resolved mapping)
// - IsFavourite (from AniList target in mapping)
// - IsFavourite (from AniList target in mapping).
func (a *App) buildFavoritesListFromMappings(
malList []Anime,
mappings []resolvedMapping,
Expand Down
5 changes: 3 additions & 2 deletions arm_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import (
"github.com/stretchr/testify/assert"
)

func writeJSON(t *testing.T, w http.ResponseWriter, v interface{}) {
func writeJSON(t *testing.T, w http.ResponseWriter, v any) {
t.Helper()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(v); err != nil {
err := json.NewEncoder(w).Encode(v)
if err != nil {
t.Fatalf("failed to encode JSON response: %v", err)
}
}
Expand Down
Loading