Skip to content

refactor: consolidate version parsing and discover backup apps dynamically#3709

Closed
usvimal wants to merge 1 commit intospicetify:mainfrom
usvimal:refactor/version-parsing-dynamic-backup
Closed

refactor: consolidate version parsing and discover backup apps dynamically#3709
usvimal wants to merge 1 commit intospicetify:mainfrom
usvimal:refactor/version-parsing-dynamic-backup

Conversation

@usvimal
Copy link

@usvimal usvimal commented Feb 23, 2026

Summary

  • Add utils.SpotifyVersion type with ParseSpotifyVersion() and AtLeast() method to eliminate duplicated version parsing logic that was copy-pasted in both preprocess.go and apply.go.
  • Add utils/constants.go with app name constants (AppXPUI, AppLogin) and KnownApps slice.
  • Make backup.Extract() discover .spa files dynamically via filepath.Glob("*.spa") instead of hardcoding ["xpui", "login"]. This means new Spotify apps are automatically extracted without code changes.

Files Changed

  • src/utils/version.go — New SpotifyVersion type
  • src/utils/constants.go — New app name constants
  • src/apply/apply.go — Use ParseSpotifyVersion, remove unused strconv
  • src/preprocess/preprocess.go — Use ParseSpotifyVersion
  • src/backup/backup.go — Dynamic .spa discovery via filepath.Glob

Test plan

  • spicetify apply correctly applies version-gated patches
  • spicetify backup + spicetify restore round-trips correctly
  • Backup extraction handles both xpui.spa and login.spa

Summary by CodeRabbit

  • Refactor
    • Consolidated Spotify version parsing logic across multiple modules for improved code maintainability and reduced duplication.
    • Enhanced backup extraction process to dynamically discover application files during extraction instead of relying on predefined file lists, providing greater flexibility for backup restoration operations.

…cally

- Add utils.SpotifyVersion type with ParseSpotifyVersion() and AtLeast()
  to eliminate duplicated version parsing in preprocess.go and apply.go
- Add utils/constants.go with app name constants (AppXPUI, AppLogin)
- Make backup.Extract() discover .spa files via filepath.Glob instead
  of hardcoding ["xpui", "login"], so new Spotify apps are handled
  automatically without code changes
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Introduces centralized Spotify version parsing utilities and refactors version parsing across multiple modules to use the new utility. Additionally, implements dynamic SPA file discovery in backup extraction, replacing hardcoded file lists. New constants define known application identifiers in a shared utilities package.

Changes

Cohort / File(s) Summary
Version Parsing Utilities
src/utils/version.go, src/utils/constants.go
New utilities module with SpotifyVersion struct, ParseSpotifyVersion() parser, and AtLeast() comparison method; introduces AppXPUI and AppLogin constants plus KnownApps slice.
Version Parsing Refactoring
src/apply/apply.go, src/preprocess/preprocess.go
Replaced manual string splitting and integer parsing with centralized utils.ParseSpotifyVersion() calls; version fields now derived from parsed SpotifyVersion struct.
SPA Discovery Refactoring
src/backup/backup.go
Changed Extract to dynamically discover SPA files via glob pattern instead of hardcoded list; derives app name from SPA filename; removes explicit path existence checks.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 ✨
Version strings now parse with care,
Through utils we refactor with flair,
SPA files dance to dynamic discovery,
Constants shared—a unified recovery!
Hop along, the code grows cleaner today. 🌟

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the two main changes: version parsing consolidation and dynamic backup app discovery, which align with the PR's actual modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rxri
Copy link
Member

rxri commented Feb 23, 2026

since this is ai slop like other prs - I'm rejecting it. maybe if you would have done this manually since this is literally not a huge task I might have considered merging it

@rxri rxri closed this Feb 23, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

> [!CAUTION]

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backup/backup.go (1)

18-32: ⚠️ Potential issue | 🟡 Minor

Silent success when no .spa files are found

If backupPath is empty, missing, or contains no .spa files, Glob ignores file system errors such as I/O errors reading directories and returns nil, nil, so spaFiles is an empty slice. The for loop body never executes, yet spinner.Success("Extracted backup") fires unconditionally — the user sees a successful extraction even though nothing happened. This can mask a corrupt or incomplete backup.

🐛 Proposed fix
 func Extract(backupPath, extractPath string) {
 	spinner, _ := utils.Spinner.Start("Extracting backup")

 	spaFiles, _ := filepath.Glob(filepath.Join(backupPath, "*.spa"))
+	if len(spaFiles) == 0 {
+		spinner.Fail("No .spa files found in backup")
+		utils.Fatal(fmt.Errorf("backup at %s contains no .spa files", backupPath))
+	}
 	for _, spaFile := range spaFiles {
 		appName := strings.TrimSuffix(filepath.Base(spaFile), ".spa")
 		appExtractToFolder := filepath.Join(extractPath, appName)

 		err := utils.Unzip(spaFile, appExtractToFolder)
 		if err != nil {
 			spinner.Fail("Failed to extract backup")
 			utils.Fatal(err)
 		}
 	}
 	spinner.Success("Extracted backup")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backup/backup.go` around lines 18 - 32, In Extract, handle the
filepath.Glob error and the empty-result case so we don't report success when
nothing was extracted: after calling filepath.Glob(filepath.Join(backupPath,
"*.spa")) check the returned error and if non-nil call spinner.Fail(...) and
utils.Fatal(err); then if len(spaFiles) == 0 call spinner.Fail("No .spa files
found in backup") (or similar) and exit/fatalize instead of proceeding to
spinner.Success; reference the Extract function, spaFiles, spinner and
backupPath when making these checks.
🧹 Nitpick comments (3)
src/utils/version.go (2)

33-41: AtLeast is never called; callers retain semantically incorrect version comparisons

AtLeast is introduced to fix a real correctness gap, but apply.go (Line 55) and preprocess.go (Lines 224, 231, 238) were not migrated to use it. The raw chained checks — e.g., spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 — are wrong for any Spotify release that bumps minor past 2 (e.g., 1.3.0: minor 3 >= 2 ✓ but patch 0 >= 57 ✗ → whole condition false, even though 1.3.0 > 1.2.57). AtLeast handles this correctly; please migrate the callers.

♻️ Example migration for apply.go Line 55 and preprocess.go Lines 224, 231, 238

In src/apply/apply.go:

-	if spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 {
+	if spotifyVer.AtLeast(1, 2, 57) {

In src/preprocess/preprocess.go:

-				if spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57 {
+				if spotifyVer.AtLeast(1, 2, 57) {
-				if spotifyMajor >= 1 && spotifyMinor >= 2 && (spotifyPatch >= 28 && spotifyPatch <= 57) {
+				if spotifyVer.AtLeast(1, 2, 28) && !spotifyVer.AtLeast(1, 2, 58) {
-				if spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch < 78 {
+				if !spotifyVer.AtLeast(1, 2, 78) {

Once spotifyMajor, spotifyMinor, spotifyPatch are no longer referenced, the unpacking assignments on the lines immediately following ParseSpotifyVersion calls can also be removed, leaving just spotifyVer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/version.go` around lines 33 - 41, Replace the incorrect chained
numeric comparisons with the SpotifyVersion.AtLeast method: after calling
ParseSpotifyVersion keep the SpotifyVersion value (e.g., spotifyVer) and change
conditions like "spotifyMajor >= 1 && spotifyMinor >= 2 && spotifyPatch >= 57"
to "spotifyVer.AtLeast(1,2,57)"; update all callers (apply.go and preprocess.go
branches referenced in the review) to use spotifyVer.AtLeast(...) and remove any
now-unused unpacking of spotifyMajor/spotifyMinor/spotifyPatch returned by
ParseSpotifyVersion so only spotifyVer is kept.

20-22: len(parts) > 0 guard is always true

strings.Split never returns an empty slice — for any input (including "") it returns at least [""]. The check on Line 20 is therefore always true and can be removed.

♻️ Simplified ParseSpotifyVersion
 func ParseSpotifyVersion(ver string) SpotifyVersion {
 	ver = strings.TrimPrefix(ver, "v")
 	parts := strings.Split(ver, ".")
 	v := SpotifyVersion{}
-	if len(parts) > 0 {
-		v.Major, _ = strconv.Atoi(parts[0])
-	}
+	v.Major, _ = strconv.Atoi(parts[0])
 	if len(parts) > 1 {
 		v.Minor, _ = strconv.Atoi(parts[1])
 	}
 	if len(parts) > 2 {
 		v.Patch, _ = strconv.Atoi(parts[2])
 	}
 	return v
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/version.go` around lines 20 - 22, The `len(parts) > 0` guard in
ParseSpotifyVersion is redundant because strings.Split always returns at least
[""]; remove the if statement and directly assign v.Major using parts[0] (i.e.,
v.Major, _ = strconv.Atoi(parts[0])), keeping the existing error-ignoring
behavior so behavior is unchanged; reference symbols: ParseSpotifyVersion,
parts, v.Major, strconv.Atoi.
src/utils/constants.go (1)

10-11: Remove unused constants and consider immutable API design for registry data

KnownApps, AppXPUI, and AppLogin are not referenced anywhere in the codebase and should be removed as dead code.

Additionally, if a registry of known app identifiers is needed in the future, avoid exposing mutable exported slices. Instead, return a copy via a function or keep the slice unexported with a getter function to prevent accidental or malicious modification of global state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/constants.go` around lines 10 - 11, Remove the dead exported
symbols AppXPUI, AppLogin, and KnownApps from constants.go (they are unused); if
you need a registry later, make the slice unexported (e.g., knownApps) and
provide a getter function (e.g., func KnownApps() []string) that returns a copy
of the slice to preserve immutability and prevent external mutation, ensuring
all references use the getter instead of the raw slice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/backup/backup.go`:
- Around line 18-32: In Extract, handle the filepath.Glob error and the
empty-result case so we don't report success when nothing was extracted: after
calling filepath.Glob(filepath.Join(backupPath, "*.spa")) check the returned
error and if non-nil call spinner.Fail(...) and utils.Fatal(err); then if
len(spaFiles) == 0 call spinner.Fail("No .spa files found in backup") (or
similar) and exit/fatalize instead of proceeding to spinner.Success; reference
the Extract function, spaFiles, spinner and backupPath when making these checks.

---

Nitpick comments:
In `@src/utils/constants.go`:
- Around line 10-11: Remove the dead exported symbols AppXPUI, AppLogin, and
KnownApps from constants.go (they are unused); if you need a registry later,
make the slice unexported (e.g., knownApps) and provide a getter function (e.g.,
func KnownApps() []string) that returns a copy of the slice to preserve
immutability and prevent external mutation, ensuring all references use the
getter instead of the raw slice.

In `@src/utils/version.go`:
- Around line 33-41: Replace the incorrect chained numeric comparisons with the
SpotifyVersion.AtLeast method: after calling ParseSpotifyVersion keep the
SpotifyVersion value (e.g., spotifyVer) and change conditions like "spotifyMajor
>= 1 && spotifyMinor >= 2 && spotifyPatch >= 57" to
"spotifyVer.AtLeast(1,2,57)"; update all callers (apply.go and preprocess.go
branches referenced in the review) to use spotifyVer.AtLeast(...) and remove any
now-unused unpacking of spotifyMajor/spotifyMinor/spotifyPatch returned by
ParseSpotifyVersion so only spotifyVer is kept.
- Around line 20-22: The `len(parts) > 0` guard in ParseSpotifyVersion is
redundant because strings.Split always returns at least [""]; remove the if
statement and directly assign v.Major using parts[0] (i.e., v.Major, _ =
strconv.Atoi(parts[0])), keeping the existing error-ignoring behavior so
behavior is unchanged; reference symbols: ParseSpotifyVersion, parts, v.Major,
strconv.Atoi.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a6b235 and dba890e.

📒 Files selected for processing (5)
  • src/apply/apply.go
  • src/backup/backup.go
  • src/preprocess/preprocess.go
  • src/utils/constants.go
  • src/utils/version.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants