From c14df8c2fb676d733c25e982f8478b7da6e10249 Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Fri, 13 Jun 2025 05:36:41 -0400 Subject: [PATCH] feat: time-gated imports via cfg --- internal/cfg/cfg.go | 20 ++++++++++++++++++++ internal/importer/importer.go | 17 +++++++++++++++++ internal/importer/lastfm.go | 4 ++++ internal/importer/listenbrainz.go | 7 ++++++- internal/importer/maloja.go | 8 ++++++-- internal/importer/spotify.go | 7 ++++++- 6 files changed, 59 insertions(+), 4 deletions(-) diff --git a/internal/cfg/cfg.go b/internal/cfg/cfg.go index de27c2e..b976d24 100644 --- a/internal/cfg/cfg.go +++ b/internal/cfg/cfg.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" "sync" + "time" ) const ( @@ -37,6 +38,8 @@ const ( ALLOWED_HOSTS_ENV = "KOITO_ALLOWED_HOSTS" DISABLE_RATE_LIMIT_ENV = "KOITO_DISABLE_RATE_LIMIT" THROTTLE_IMPORTS_MS = "KOITO_THROTTLE_IMPORTS_MS" + IMPORT_BEFORE_UNIX_ENV = "KOITO_IMPORT_BEFORE_UNIX" + IMPORT_AFTER_UNIX_ENV = "KOITO_IMPORT_AFTER_UNIX" ) type config struct { @@ -64,6 +67,8 @@ type config struct { disableRateLimit bool importThrottleMs int userAgent string + importBefore time.Time + importAfter time.Time } var ( @@ -112,6 +117,16 @@ func loadConfig(getenv func(string) string) (*config, error) { cfg.lbzRelayUrl = getenv(LBZ_RELAY_URL_ENV) } + beforeutx, _ := strconv.ParseInt(getenv(IMPORT_BEFORE_UNIX_ENV), 10, 64) + afterutx, _ := strconv.ParseInt(getenv(IMPORT_AFTER_UNIX_ENV), 10, 64) + + if beforeutx > 0 { + cfg.importBefore = time.Unix(beforeutx, 0) + } + if afterutx > 0 { + cfg.importAfter = time.Unix(afterutx, 0) + } + cfg.importThrottleMs, _ = strconv.Atoi(getenv(THROTTLE_IMPORTS_MS)) cfg.disableRateLimit = parseBool(getenv(DISABLE_RATE_LIMIT_ENV)) @@ -308,3 +323,8 @@ func ThrottleImportMs() int { defer lock.RUnlock() return globalConfig.importThrottleMs } + +// returns the before, after times, in that order +func ImportWindow() (time.Time, time.Time) { + return globalConfig.importBefore, globalConfig.importAfter +} diff --git a/internal/importer/importer.go b/internal/importer/importer.go index 306f7a6..f27cf21 100644 --- a/internal/importer/importer.go +++ b/internal/importer/importer.go @@ -4,11 +4,13 @@ import ( "context" "os" "path" + "time" "github.com/gabehf/koito/internal/cfg" "github.com/gabehf/koito/internal/logger" ) +// runs after every importer func finishImport(ctx context.Context, filename string, numImported int) error { l := logger.FromContext(ctx) _, err := os.Stat(path.Join(cfg.ConfigDir(), "import_complete")) @@ -27,3 +29,18 @@ func finishImport(ctx context.Context, filename string, numImported int) error { } return nil } + +// from https://stackoverflow.com/a/55093788 with modification to use cfg and check for zero values +func inImportTimeWindow(check time.Time) bool { + end, start := cfg.ImportWindow() + if start.IsZero() && end.IsZero() { + return true + } + if !start.IsZero() && end.IsZero() { + return !check.Before(start) + } + if start.IsZero() && !end.IsZero() { + return !check.After(end) + } + return !check.Before(start) && !check.After(end) +} diff --git a/internal/importer/lastfm.go b/internal/importer/lastfm.go index 6a4bb93..1fd6d7c 100644 --- a/internal/importer/lastfm.go +++ b/internal/importer/lastfm.go @@ -93,6 +93,10 @@ func ImportLastFMFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrainzCall } else { ts = time.Unix(unix, 0).UTC() } + if !inImportTimeWindow(ts) { + l.Debug().Msgf("Skipping import due to import time rules") + continue + } opts := catalog.SubmitListenOpts{ MbzCaller: mbzc, Artist: track.Artist.Text, diff --git a/internal/importer/listenbrainz.go b/internal/importer/listenbrainz.go index 19c81a3..c9b7355 100644 --- a/internal/importer/listenbrainz.go +++ b/internal/importer/listenbrainz.go @@ -78,6 +78,11 @@ func ImportListenBrainzFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrai fmt.Println("Error unmarshaling JSON:", err) continue } + ts := time.Unix(payload.ListenedAt, 0) + if !inImportTimeWindow(ts) { + l.Debug().Msgf("Skipping import due to import time rules") + continue + } artistMbzIDs, err := utils.ParseUUIDSlice(payload.TrackMeta.AdditionalInfo.ArtistMBIDs) if err != nil { l.Debug().Err(err).Msg("Failed to parse one or more uuids") @@ -119,7 +124,7 @@ func ImportListenBrainzFile(ctx context.Context, store db.DB, mbzc mbz.MusicBrai ReleaseMbzID: releaseMbzID, ReleaseGroupMbzID: rgMbzID, Duration: duration, - Time: time.Unix(payload.ListenedAt, 0), + Time: ts, UserID: 1, Client: client, } diff --git a/internal/importer/maloja.go b/internal/importer/maloja.go index f14eeb5..3343af7 100644 --- a/internal/importer/maloja.go +++ b/internal/importer/maloja.go @@ -65,14 +65,18 @@ func ImportMalojaFile(ctx context.Context, store db.DB, filename string) error { l.Debug().Msg("Skipping invalid maloja import item") continue } - ts := time.Unix(item.Time, 0).UTC() + ts := time.Unix(item.Time, 0) + if !inImportTimeWindow(ts) { + l.Debug().Msgf("Skipping import due to import time rules") + continue + } opts := catalog.SubmitListenOpts{ MbzCaller: &mbz.MusicBrainzClient{}, Artist: item.Track.Artists[0], ArtistNames: artists, TrackTitle: item.Track.Title, ReleaseTitle: item.Track.Album.Title, - Time: ts, + Time: ts.Local(), UserID: 1, } err = catalog.SubmitListen(ctx, store, opts) diff --git a/internal/importer/spotify.go b/internal/importer/spotify.go index 6facce1..9b2cd81 100644 --- a/internal/importer/spotify.go +++ b/internal/importer/spotify.go @@ -43,10 +43,15 @@ func ImportSpotifyFile(ctx context.Context, store db.DB, filename string) error if err != nil { return err } + for _, item := range export { if item.ReasonEnd != "trackdone" { continue } + if !inImportTimeWindow(item.Timestamp) { + l.Debug().Msgf("Skipping import due to import time rules") + continue + } dur := item.MsPlayed if item.TrackName == "" || item.ArtistName == "" { l.Debug().Msg("Skipping non-track item") @@ -58,7 +63,7 @@ func ImportSpotifyFile(ctx context.Context, store db.DB, filename string) error TrackTitle: item.TrackName, ReleaseTitle: item.AlbumName, Duration: dur / 1000, - Time: item.Timestamp.UTC(), + Time: item.Timestamp, UserID: 1, } err = catalog.SubmitListen(ctx, store, opts)