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"
' client version when empty primaryImageTag entries were cleaned up
const EMPTY_IMAGE_TAG_CLEANUP_VERSION = "1.4.0"
' client version when uiHomeSplashBackground setting was removed
const SPLASH_SETTING_REMOVAL_VERSION = "1.5.0"
' client version when globalSplashScreen was incorrectly saved to user registry sections
const GLOBAL_SETTINGS_CLEANUP_VERSION = "1.5.2"
' 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
' EMPTY_IMAGE_TAG_CLEANUP_VERSION - Remove empty primaryImageTag entries
if isValid(lastRunVersion) and not versionChecker(lastRunVersion, EMPTY_IMAGE_TAG_CLEANUP_VERSION)
m.wasMigrated = true
' Delete empty or whitespace-only primaryImageTag values
' Empty tags were incorrectly stored by buggy code and prevent placeholder images from showing
settingName = "primaryImageTag"
if reg.exists(settingName)
value = reg.read(settingName)
' Check if value is empty or whitespace-only (using same logic as isValidAndNotEmpty)
if not isValid(value) or value.trim() = ""
print `Cleaning up empty primaryImageTag for userid: ${section}`
reg.delete(settingName)
reg.flush()
print `Deleted empty primaryImageTag value`
else
print `No primaryImageTag cleanup needed for userid: ${section} (value is valid)`
end if
else
print `No primaryImageTag cleanup needed for userid: ${section} (setting not found)`
end if
end if
' SPLASH_SETTING_REMOVAL_VERSION - Remove deprecated uiHomeSplashBackground setting
if isValid(lastRunVersion) and not versionChecker(lastRunVersion, SPLASH_SETTING_REMOVAL_VERSION)
m.wasMigrated = true
' Delete deprecated splash background user setting
' Splash now always displays on UserSelect when server has it enabled
settingName = "uiHomeSplashBackground"
if reg.exists(settingName)
print `Removing deprecated ${settingName} setting for userid: ${section}`
reg.delete(settingName)
reg.flush()
print `Deleted deprecated splash background setting`
else
print `No splash background setting removal needed for userid: ${section} (setting not found)`
end if
end if
' GLOBAL_SETTINGS_CLEANUP_VERSION - Remove globalSplashScreen from user registry sections
if isValid(lastRunVersion) and not versionChecker(lastRunVersion, GLOBAL_SETTINGS_CLEANUP_VERSION)
m.wasMigrated = true
' globalSplashScreen was incorrectly saved to user registry sections due to a bug in radioSettingChanged()
' This setting should ONLY exist in the global "JellyRock" or "test-global" section
' Delete it from user sections to prevent confusion and ensure single source of truth
settingName = "globalSplashScreen"
if reg.exists(settingName)
print `Cleaning up incorrectly saved ${settingName} for userid: ${section}`
reg.delete(settingName)
reg.flush()
print `Deleted ${settingName} from user ${section}`
else
print `No global splash screen cleanup needed for userid: ${section} (setting not found)`
end if
end if
end for
end sub