source_migrations.bs

import "pkg:/source/utils/conditional.bs"
import "pkg:/source/utils/misc.bs"

' @fileoverview Functions that update the registry based on the last run version and the currently running version

' client version when settings were renamed from dotted names to camelCase
const SETTINGS_MIGRATION_VERSION = "1.1.0"

' client version when audio codec preference was renamed and migrated
const AUDIO_CODEC_MIGRATION_VERSION = "1.1.5"

' Run all necessary registry migrations on the "global" JellyRock registry section
sub runGlobalMigrations()
  appLastRunVersion = m.global.app.lastRunVersion
  ' SETTINGS_MIGRATION_VERSION
  if isValid(appLastRunVersion) and not versionChecker(appLastRunVersion, SETTINGS_MIGRATION_VERSION)
    ' last app version used < SETTINGS_MIGRATION_VERSION
    m.wasMigrated = true

    migrations = {
      ' Global settings
      "global.rememberme": "globalRememberMe"
    }

    ' Migrate each setting: read → write new → delete old
    reg = CreateObject("roRegistrySection", getGlobalRegistrySection())
    migratedCount = 0
    for each oldName in migrations
      if reg.exists(oldName)
        if migratedCount = 0
          print `Running ${SETTINGS_MIGRATION_VERSION} global registry migrations`
        end if
        value = reg.read(oldName)
        newName = migrations[oldName]
        reg.write(newName, value)
        reg.delete(oldName)
        migratedCount++
      end if
    end for

    if migratedCount > 0
      reg.flush()
    end if

  end if
end sub

' Run registry migrations for user sections
' Only processes sections with a valid serverId key (written by user.Login())
' Automatically skips orphaned/incomplete sections and global sections
' @param targetSections Optional array of specific section names to migrate. If invalid, migrates all sections.
sub runRegistryUserMigrations(targetSections = invalid as dynamic)
  regSections = invalid

  ' If specific sections provided, use those; otherwise get all sections
  if isValid(targetSections) and type(targetSections) = "roArray"
    regSections = targetSections
  else
    regSections = getRegistrySections()
  end if

  ' Detect test mode ONCE: if ANY section starts with "test-", we're in test mode
  ' In test mode, ONLY migrate test sections to prevent touching real user data
  hasTestSections = false
  for each checkSection in regSections
    if LCase(checkSection).left(5) = "test-"
      hasTestSections = true
      exit for
    end if
  end for

  for each section in regSections
    ' Skip global registry sections (JellyRock and test-global)
    sectionLower = LCase(section)
    if sectionLower = "jellyrock" or sectionLower = "test-global"
      continue for
    end if

    ' In test mode, skip non-test user sections
    isTestSection = sectionLower.left(5) = "test-"
    if hasTestSections and not isTestSection
      continue for
    end if

    ' Process this section
    reg = CreateObject("roRegistrySection", section)

    ' Validate this is a legitimate user section by checking for serverId
    ' serverId is ALWAYS written by user.Login() regardless of saveCredentials
    ' If missing, this is an orphaned/incomplete section - skip it
    if not reg.exists("serverId")
      print `Skipping incomplete user section: ${section} (missing serverId)`
      continue for
    end if

    ' Check if user needs migrations
    lastRunVersion = invalid
    if reg.exists("LastRunVersion")
      lastRunVersion = reg.read("LastRunVersion")
    else
      ' No LastRunVersion found - could be old user data needing migration
      ' Use "0.0.0" to trigger all migrations (safe since migrations are idempotent)
      lastRunVersion = "0.0.0"
      reg.write("LastRunVersion", lastRunVersion)
    end if

    ' SETTINGS_MIGRATION_VERSION - Rename dotted settings to camelCase
    if isValid(lastRunVersion) and not versionChecker(lastRunVersion, SETTINGS_MIGRATION_VERSION)
      m.wasMigrated = true

      ' Define user setting migrations (old dotted name → new camelCase name)
      ' Single source of truth for migration mapping
      migrations = {
        ' Auth settings (stored per-user for multi-user support)
        "token": "authToken",
        "primaryimagetag": "primaryImageTag",

        ' Playback settings
        "playback.bitrate.maxlimited": "playbackBitrateMaxLimited",
        "playback.bitrate.limit": "playbackBitrateLimit",
        "playback.cinemamode": "playbackCinemaMode",
        "playback.subs.custom": "playbackSubsCustom",
        "playback.resolution.max": "playbackResolutionMax",
        "playback.nextupbuttonseconds": "playbackNextUpButtonSeconds",
        "playback.playnextepisode": "playbackPlayNextEpisode",
        "playback.preferredAudioCodec": "playbackPreferredAudioCodec",
        "playback.subs.onlytext": "playbackSubsOnlyText",
        "playback.mpeg2": "playbackMpeg2",
        "playback.mpeg4": "playbackMpeg4",
        "playback.tryDirect.h264ProfileLevel": "playbackTryDirectH264ProfileLevel",
        "playback.tryDirect.hevcProfileLevel": "playbackTryDirectHevcProfileLevel",

        ' UI settings
        "ui.general.episodeimagesnextup": "uiGeneralEpisodeImages",
        "ui.font.fallback": "uiFontFallback",
        "ui.design.hideclock": "uiDesignHideClock",
        "ui.details.maxdaysnextup": "uiDetailsMaxDaysNextUp",
        "ui.details.enablerewatchingnextup": "uiDetailsEnableRewatchingNextUp",
        "ui.row.layout": "uiRowLayout",
        "ui.home.splashBackground": "uiHomeSplashBackground",
        "ui.home.useWebSectionArrangement": "uiHomeUseWebArrangement",
        "ui.details.hidetagline": "uiDetailsHideTagline",
        "ui.movies.showRatings": "uiMoviesShowRatings",
        "ui.tvshows.blurunwatched": "uiTvShowsBlurUnwatched",
        "ui.tvshows.disableCommunityRating": "uiTvShowsDisableCommunityRating",
        "ui.tvshows.disableUnwatchedEpisodeCount": "uiTvShowsDisableUnwatchedCount",
        "ui.tvshows.goStraightToEpisodeListing": "uiTvShowsGoStraightToEpisodes",

        ' Item grid settings
        "itemgrid.showItemCount": "itemGridShowItemCount",
        "itemgrid.gridTitles": "itemGridTitles",
        "itemgrid.reset": "itemGridReset",
        "itemgrid.movieDefaultView": "itemGridMovieDefaultView",

        ' Home section settings
        "homesection0": "homeSection0",
        "homesection1": "homeSection1",
        "homesection2": "homeSection2",
        "homesection3": "homeSection3",
        "homesection4": "homeSection4",
        "homesection5": "homeSection5",
        "homesection6": "homeSection6",

        ' Display settings
        "display.livetv.landing": "displayLiveTvLanding"
      }

      ' Migrate each setting: read → write new → delete old
      migratedCount = 0
      for each oldName in migrations
        if reg.exists(oldName)
          if migratedCount = 0
            print `Migrating user settings to v${SETTINGS_MIGRATION_VERSION} for userid: ${section}`
          end if
          value = reg.read(oldName)
          newName = migrations[oldName]
          reg.write(newName, value)
          reg.delete(oldName)
          migratedCount++
        end if
      end for

      if migratedCount > 0
        print `Successfully migrated ${migratedCount} settings for user ${section}`
      end if

      ' Delete server policy keys that should never be in registry
      ' These were incorrectly stored as user settings but are server-authoritative
      serverPolicyKeys = ["liveTvCanRecord", "contentCanDelete", "livetv.canrecord", "content.candelete"]
      deletedCount = 0
      for each key in serverPolicyKeys
        if reg.exists(key)
          reg.delete(key)
          deletedCount++
        end if
      end for

      if deletedCount > 0
        print `Deleted ${deletedCount} server policy keys from user ${section}`
      end if

      reg.flush()
    end if

    ' AUDIO_CODEC_MIGRATION_VERSION - Rename and migrate audio codec preference
    if isValid(lastRunVersion) and not versionChecker(lastRunVersion, AUDIO_CODEC_MIGRATION_VERSION)
      m.wasMigrated = true

      ' Rename playbackPreferredAudioCodec to playbackPreferredMultichannelCodec
      ' and migrate deprecated values (auto, aac) to new default (eac3)
      oldSettingName = "playbackPreferredAudioCodec"
      newSettingName = "playbackPreferredMultichannelCodec"

      if reg.exists(oldSettingName)
        print `Migrating audio codec settings to v${AUDIO_CODEC_MIGRATION_VERSION} for userid: ${section}`
        oldValue = reg.read(oldSettingName)

        ' Migrate deprecated values (auto, aac) to eac3, otherwise preserve existing value
        newValue = "eac3"
        if isValid(oldValue) and oldValue <> "" and oldValue <> "auto" and oldValue <> "aac"
          ' User has a valid preference (ac3, eac3, dts) or custom value - preserve it
          newValue = oldValue
        end if

        reg.write(newSettingName, newValue)
        reg.delete(oldSettingName)
        print `Migrated ${oldSettingName}='${oldValue}' to ${newSettingName}='${newValue}'`
        reg.flush()
      else
        print `No audio codec migration needed for userid: ${section} (setting not found)`
      end if
    end if
  end for
end sub