From 209dd614427a09cb97e270bce9ba944ef4e6f078 Mon Sep 17 00:00:00 2001
From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
Date: Tue, 30 Jun 2026 08:07:04 +0200
Subject: [PATCH] refactor(lib): consolidate generic infra into src/lib
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Move domain-agnostic, feature-free helpers out of the flat utils/ and store/
roots into src/lib/ (plan M4, §3 lib/ layer):
- lib/format/: formatBytes, formatClockTime, formatDuration, formatHumanDuration,
relativeTime (pure format/date/byte/clock helpers)
- lib/i18n.ts: i18next bootstrap (global app infra)
- lib/util/: sanitizeHtml, platform, dedupeById, safeStorage (pure helpers +
the zustand storage adapter)
playbackScheduleFormat stays in utils/format (runtime usePlayerStore dep =
audio-core coupling, not generic). formatClockTime keeps a type-only
@/store/authStoreTypes import (erased, no runtime inversion — accepted per the
deviceSync precedent).
Pure move via deep @/lib/* specifiers (no barrel → no mock-collapse surface).
tsc 0, lint 0/0, full suite 319 files / 2353 tests green.
Tooling note: lib_move.py regex extended to also rewrite side-effect imports
(import './i18n') and vi.importActual paths — both were silent gaps.
---
src/app/AppShell.tsx | 2 +-
src/app/MainApp.tsx | 2 +-
src/app/MiniPlayerApp.test.tsx | 4 ++--
src/app/MiniPlayerApp.tsx | 2 +-
src/components/AppUpdater.tsx | 2 +-
src/components/BecauseYouLikeRail.tsx | 2 +-
src/components/MobilePlayerView.tsx | 2 +-
src/components/PlaybackDelayModal.tsx | 2 +-
src/components/SongInfoModal.tsx | 4 ++--
src/components/SongRail.tsx | 2 +-
src/components/SongRow.tsx | 2 +-
src/components/TitleBar.tsx | 2 +-
src/components/playerBar/PlaybackClock.tsx | 2 +-
.../playerBar/PlayerSeekbarSection.tsx | 2 +-
src/components/queuePanel/QueueHeader.tsx | 4 ++--
src/components/queuePanel/QueueList.tsx | 2 +-
src/config/shortcutActionRegistry.ts | 2 +-
src/config/shortcutDispatch.ts | 2 +-
src/features/album/components/AlbumHeader.tsx | 6 ++---
src/features/album/components/AlbumRow.tsx | 2 +-
.../album/components/AlbumTrackListMobile.tsx | 2 +-
src/features/album/components/TrackRow.tsx | 4 ++--
.../album/hooks/useAlbumBrowseData.ts | 2 +-
.../album/hooks/useGenreAlbumBrowse.ts | 2 +-
src/features/album/pages/RandomAlbums.tsx | 2 +-
.../components/ArtistDetailTopTracks.tsx | 2 +-
.../artist/hooks/useArtistsBrowseCatalog.ts | 2 +-
.../components/DeviceSyncPreSyncModal.tsx | 2 +-
.../utils/deviceSyncLegacyTemplate.ts | 2 +-
.../utils/runDeviceSyncMigration.ts | 2 +-
.../favorites/components/FavoriteSongRow.tsx | 4 ++--
.../fullscreenPlayer/components/FsClock.tsx | 2 +-
.../components/FsQueueModal.tsx | 2 +-
.../components/FsTimeReadout.tsx | 2 +-
.../miniPlayer/components/MiniControls.tsx | 2 +-
.../miniPlayer/components/MiniTitlebar.tsx | 2 +-
.../miniPlayer/hooks/useMiniWindowSetup.ts | 2 +-
.../nowPlaying/components/AlbumCard.tsx | 2 +-
src/features/nowPlaying/components/Hero.tsx | 2 +-
.../nowPlaying/components/RadioView.tsx | 2 +-
.../nowPlaying/components/TopSongsCard.tsx | 2 +-
.../components/OfflineLibraryDiskStat.tsx | 2 +-
.../offline/components/ZipDownloadOverlay.tsx | 2 +-
.../offline/utils/favoritesOfflineSync.ts | 2 +-
.../offline/utils/offlineStarredLoad.ts | 2 +-
.../orbit/components/OrbitSessionBar.tsx | 2 +-
src/features/orbit/hooks/useOrbitGuest.ts | 2 +-
src/features/orbit/hooks/useOrbitHost.ts | 2 +-
src/features/orbit/utils/orbitBulkGuard.ts | 2 +-
.../playlist/components/PlaylistCard.tsx | 2 +-
.../playlist/components/PlaylistRow.tsx | 4 ++--
.../components/PlaylistSongSearchPanel.tsx | 2 +-
.../components/PlaylistSuggestions.tsx | 4 ++--
.../components/ShareQueuePreviewModal.tsx | 2 +-
.../settings/components/AppearanceTab.tsx | 2 +-
src/features/settings/components/AudioTab.tsx | 2 +-
.../components/CoverCacheStrategySection.tsx | 2 +-
.../settings/components/StorageTab.tsx | 2 +-
.../settings/components/SystemTab.tsx | 4 ++--
.../settings/components/ThemeStoreSection.tsx | 2 +-
src/features/settings/pages/Settings.tsx | 2 +-
.../components/PlayerStatisticsPanel.tsx | 2 +-
.../stats/components/PlayerStatsDayTracks.tsx | 2 +-
.../components/PlayerStatsRecentDays.tsx | 2 +-
src/features/stats/pages/Statistics.tsx | 2 +-
src/hooks/tauriBridge/useCliBridge.ts | 2 +-
src/hooks/useAppUpdater.ts | 2 +-
src/hooks/useAudioDevicesProbe.ts | 2 +-
src/hooks/usePlatformShellSetup.ts | 2 +-
src/hooks/useThemeAnimationRisk.ts | 2 +-
src/{utils => lib}/format/formatBytes.ts | 0
.../format/formatClockTime.test.ts | 2 +-
src/{utils => lib}/format/formatClockTime.ts | 2 +-
.../format/formatDuration.test.ts | 2 +-
src/{utils => lib}/format/formatDuration.ts | 0
.../format/formatHumanDuration.test.ts | 4 ++--
.../format/formatHumanDuration.ts | 2 +-
.../format/relativeTime.test.ts | 2 +-
src/{utils => lib}/format/relativeTime.ts | 0
src/{ => lib}/i18n.ts | 24 +++++++++----------
src/{utils => lib/util}/dedupeById.ts | 0
src/{utils => lib/util}/platform.ts | 0
src/{store => lib/util}/safeStorage.test.ts | 2 +-
src/{store => lib/util}/safeStorage.ts | 0
src/{utils => lib/util}/sanitizeHtml.ts | 0
src/main.tsx | 2 +-
src/pages/ComposerDetail.tsx | 2 +-
src/pages/Home.tsx | 2 +-
src/pages/Login.tsx | 2 +-
src/pages/NewReleases.tsx | 2 +-
src/store/authStoreRehydrate.ts | 2 +-
src/store/miscActions.ts | 2 +-
src/store/playerStore.ts | 2 +-
src/test/helpers/renderWithProviders.tsx | 2 +-
.../componentHelpers/appUpdaterHelpers.ts | 2 +-
.../componentHelpers/nowPlayingHelpers.ts | 2 +-
.../componentHelpers/playlistDetailHelpers.ts | 4 ++--
src/utils/componentHelpers/userMgmtHelpers.ts | 2 +-
src/utils/library/albumBrowseCatalogChunk.ts | 2 +-
src/utils/library/albumBrowseLocal.ts | 2 +-
src/utils/library/albumBrowseNetwork.ts | 2 +-
src/utils/mix/luckyMix.ts | 2 +-
src/utils/perf/perfLiveCpuSnapshot.ts | 2 +-
103 files changed, 119 insertions(+), 119 deletions(-)
rename src/{utils => lib}/format/formatBytes.ts (100%)
rename src/{utils => lib}/format/formatClockTime.test.ts (95%)
rename src/{utils => lib}/format/formatClockTime.ts (91%)
rename src/{utils => lib}/format/formatDuration.test.ts (98%)
rename src/{utils => lib}/format/formatDuration.ts (100%)
rename src/{utils => lib}/format/formatHumanDuration.test.ts (96%)
rename src/{utils => lib}/format/formatHumanDuration.ts (98%)
rename src/{utils => lib}/format/relativeTime.test.ts (93%)
rename src/{utils => lib}/format/relativeTime.ts (100%)
rename src/{ => lib}/i18n.ts (63%)
rename src/{utils => lib/util}/dedupeById.ts (100%)
rename src/{utils => lib/util}/platform.ts (100%)
rename src/{store => lib/util}/safeStorage.test.ts (96%)
rename src/{store => lib/util}/safeStorage.ts (100%)
rename src/{utils => lib/util}/sanitizeHtml.ts (100%)
diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx
index 57a67314..66b0df52 100644
--- a/src/app/AppShell.tsx
+++ b/src/app/AppShell.tsx
@@ -54,7 +54,7 @@ import { useNowPlayingPrewarm } from '@/features/nowPlaying';
import { useOfflineAutoNav } from '@/features/offline';
import { useOfflineLibraryFilterSuspend } from '@/features/offline';
import { AppShellQueueResizerSeam } from '../components/AppShellQueueResizerSeam';
-import { IS_LINUX, IS_MACOS } from '../utils/platform';
+import { IS_LINUX, IS_MACOS } from '@/lib/util/platform';
import { useConnectionStatus } from '../hooks/useConnectionStatus';
import { useIdlePlayQueuePull } from '../hooks/useIdlePlayQueuePull';
import { useAuthStore } from '../store/authStore';
diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx
index a960f51f..78532e7c 100644
--- a/src/app/MainApp.tsx
+++ b/src/app/MainApp.tsx
@@ -29,7 +29,7 @@ import { useLibraryCoverBackfill } from '../hooks/useLibraryCoverBackfill';
import { useCoverRevalidateScheduler } from '../cover/useCoverRevalidateScheduler';
import { runCoverIdbUpgradeMigration } from '../utils/migrations/coverIdbUpgradeMigration';
import { useMigrationOrchestrator } from '../hooks/useMigrationOrchestrator';
-import { IS_WINDOWS } from '../utils/platform';
+import { IS_WINDOWS } from '@/lib/util/platform';
import TauriEventBridge from './TauriEventBridge';
import AppShell from './AppShell';
import ErrorBoundary from '../components/ErrorBoundary';
diff --git a/src/app/MiniPlayerApp.test.tsx b/src/app/MiniPlayerApp.test.tsx
index 2996cd8a..2747f70b 100644
--- a/src/app/MiniPlayerApp.test.tsx
+++ b/src/app/MiniPlayerApp.test.tsx
@@ -25,7 +25,7 @@ vi.mock('../store/keybindingsStore', () => ({
vi.mock('../utils/perf/perfFlags', () => ({
usePerfProbeFlags: () => ({ disableTooltipPortal: true }),
}));
-vi.mock('../i18n', () => ({
+vi.mock('@/lib/i18n', () => ({
default: { changeLanguage: vi.fn() },
}));
vi.mock('@/features/miniPlayer', () => ({ default: () =>
}));
@@ -38,7 +38,7 @@ vi.mock('../contexts/DragDropContext', () => ({
import { render, cleanup } from '@testing-library/react';
import MiniPlayerApp from './MiniPlayerApp';
-import i18n from '../i18n';
+import i18n from '@/lib/i18n';
beforeEach(() => {
themeRehydrate.mockClear();
diff --git a/src/app/MiniPlayerApp.tsx b/src/app/MiniPlayerApp.tsx
index ff8b2031..8e4d3944 100644
--- a/src/app/MiniPlayerApp.tsx
+++ b/src/app/MiniPlayerApp.tsx
@@ -8,7 +8,7 @@ import { useThemeStore } from '../store/themeStore';
import { useFontStore } from '../store/fontStore';
import { useKeybindingsStore } from '../store/keybindingsStore';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
-import i18n from '../i18n';
+import i18n from '@/lib/i18n';
/**
* Mini-player webview tree. Rendered in the secondary Tauri window labelled
diff --git a/src/components/AppUpdater.tsx b/src/components/AppUpdater.tsx
index 3beb7157..0b107a5b 100644
--- a/src/components/AppUpdater.tsx
+++ b/src/components/AppUpdater.tsx
@@ -3,7 +3,7 @@ import { open } from '@tauri-apps/plugin-shell';
import { ArrowUpCircle, CheckCircle2, ChevronDown, Download, FolderOpen, RefreshCw, ShieldCheck } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { version as currentVersion } from '../../package.json';
-import { formatBytes } from '../utils/format/formatBytes';
+import { formatBytes } from '@/lib/format/formatBytes';
import { useAppUpdater } from '../hooks/useAppUpdater';
import Modal from './Modal';
import Changelog from './appUpdater/Changelog';
diff --git a/src/components/BecauseYouLikeRail.tsx b/src/components/BecauseYouLikeRail.tsx
index 4813dcf9..45c167f0 100644
--- a/src/components/BecauseYouLikeRail.tsx
+++ b/src/components/BecauseYouLikeRail.tsx
@@ -23,7 +23,7 @@ import { useAuthStore } from '../store/authStore';
import { playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
import { useLongPressAction } from '../hooks/useLongPressAction';
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
-import { formatHumanHoursMinutes } from '../utils/format/formatHumanDuration';
+import { formatHumanHoursMinutes } from '@/lib/format/formatHumanDuration';
import { AlbumRow } from '@/features/album';
import { albumArtistDisplayName } from '@/features/album';
diff --git a/src/components/MobilePlayerView.tsx b/src/components/MobilePlayerView.tsx
index 74bc942d..d6ec4e70 100644
--- a/src/components/MobilePlayerView.tsx
+++ b/src/components/MobilePlayerView.tsx
@@ -16,7 +16,7 @@ import {
import { usePlayerStore } from '../store/playerStore';
import { useCachedUrl } from '@/ui/CachedImage';
import { OpenArtistRefInline } from '@/features/artist';
-import { formatTrackTime } from '../utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import { resolveQueueTrack } from '../utils/library/queueTrackView';
import {
getQueueResolverVersion,
diff --git a/src/components/PlaybackDelayModal.tsx b/src/components/PlaybackDelayModal.tsx
index 4a63ffb6..57f89524 100644
--- a/src/components/PlaybackDelayModal.tsx
+++ b/src/components/PlaybackDelayModal.tsx
@@ -8,7 +8,7 @@ import { useShallow } from 'zustand/react/shallow';
import type { TFunction } from 'i18next';
import { formatPlaybackScheduleRemaining } from '../utils/format/playbackScheduleFormat';
-import { formatClockTime } from '../utils/format/formatClockTime';
+import { formatClockTime } from '@/lib/format/formatClockTime';
import {
isValidPlaybackSchedulePreviewTimestamp,
parsePlaybackDelayCustomMinutes,
diff --git a/src/components/SongInfoModal.tsx b/src/components/SongInfoModal.tsx
index 5d8b3624..f51dfb23 100644
--- a/src/components/SongInfoModal.tsx
+++ b/src/components/SongInfoModal.tsx
@@ -12,7 +12,7 @@ import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { useTranslation } from 'react-i18next';
import { copyTextToClipboard } from '../utils/server/serverMagicString';
import { showToast } from '../utils/ui/toast';
-import { formatTrackTime } from '../utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import { formatLastSeen } from '../utils/componentHelpers/userMgmtHelpers';
import { libraryIsReady } from '../utils/library/libraryReady';
import {
@@ -21,7 +21,7 @@ import {
resolveQueueBpm,
type ParsedTrackEnrichment,
} from '../utils/library/trackEnrichment';
-import i18n from '../i18n';
+import i18n from '@/lib/i18n';
function formatSize(bytes?: number): string | null {
if (!bytes) return null;
diff --git a/src/components/SongRail.tsx b/src/components/SongRail.tsx
index f74ef929..21f991a1 100644
--- a/src/components/SongRail.tsx
+++ b/src/components/SongRail.tsx
@@ -3,7 +3,7 @@ import React, { useRef, useState, useEffect, useMemo } from 'react';
import { ChevronLeft, ChevronRight, RefreshCw } from 'lucide-react';
import SongCard from './SongCard';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
-import { dedupeById } from '../utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
interface Props {
title: string;
diff --git a/src/components/SongRow.tsx b/src/components/SongRow.tsx
index 45919f8d..0d4bb1b1 100644
--- a/src/components/SongRow.tsx
+++ b/src/components/SongRow.tsx
@@ -9,7 +9,7 @@ import { usePlayerStore } from '../store/playerStore';
import { enqueueAndPlay } from '../utils/playback/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '@/features/orbit';
-import { formatTrackTime } from '../utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import { resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs';
import { tooltipAttrs } from '@/ui/tooltipAttrs';
diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx
index bbde192a..1a3d33de 100644
--- a/src/components/TitleBar.tsx
+++ b/src/components/TitleBar.tsx
@@ -3,7 +3,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
import { Minus, Square, X } from 'lucide-react';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
-import { IS_MACOS } from '../utils/platform';
+import { IS_MACOS } from '@/lib/util/platform';
export default function TitleBar() {
const win = getCurrentWindow();
diff --git a/src/components/playerBar/PlaybackClock.tsx b/src/components/playerBar/PlaybackClock.tsx
index c7f9e2ac..e3f698d1 100644
--- a/src/components/playerBar/PlaybackClock.tsx
+++ b/src/components/playerBar/PlaybackClock.tsx
@@ -4,7 +4,7 @@ import {
formatPlaybarClock,
formatPlaybarToggleClock,
formatTrackTime,
-} from '../../utils/format/formatDuration';
+} from '@/lib/format/formatDuration';
/** Renders the playback clock without ever causing PlayerBar to re-render.
* Updates the DOM directly via an imperative store subscription. */
diff --git a/src/components/playerBar/PlayerSeekbarSection.tsx b/src/components/playerBar/PlayerSeekbarSection.tsx
index 9fbbc6d3..ee723481 100644
--- a/src/components/playerBar/PlayerSeekbarSection.tsx
+++ b/src/components/playerBar/PlayerSeekbarSection.tsx
@@ -2,7 +2,7 @@ import { ArrowLeftRight } from 'lucide-react';
import type { TFunction } from 'i18next';
import type { RadioMetadata } from '@/features/radio';
import { useThemeStore } from '../../store/themeStore';
-import { formatTrackTime, playbarMinuteFieldWidth } from '../../utils/format/formatDuration';
+import { formatTrackTime, playbarMinuteFieldWidth } from '@/lib/format/formatDuration';
import { WaveformSeek } from '@/features/waveform';
import { PlaybackTime, ToggleClock } from './PlaybackClock';
diff --git a/src/components/queuePanel/QueueHeader.tsx b/src/components/queuePanel/QueueHeader.tsx
index 831d204d..7f33ea13 100644
--- a/src/components/queuePanel/QueueHeader.tsx
+++ b/src/components/queuePanel/QueueHeader.tsx
@@ -7,8 +7,8 @@ import { useAuthStore } from '../../store/authStore';
import type { QueueItemRef } from '../../store/playerStoreTypes';
import type { QueueDisplayMode } from '../../store/authStoreTypes';
import type { DurationMode } from '../../utils/componentHelpers/queuePanelHelpers';
-import { formatLongDuration } from '../../utils/format/formatDuration';
-import { formatClockTime } from '../../utils/format/formatClockTime';
+import { formatLongDuration } from '@/lib/format/formatDuration';
+import { formatClockTime } from '@/lib/format/formatClockTime';
import { resolveQueueTrack } from '../../utils/library/queueTrackView';
import {
getQueueResolverVersion,
diff --git a/src/components/queuePanel/QueueList.tsx b/src/components/queuePanel/QueueList.tsx
index 8a0eb01f..d8242a50 100644
--- a/src/components/queuePanel/QueueList.tsx
+++ b/src/components/queuePanel/QueueList.tsx
@@ -7,7 +7,7 @@ import { usePlayerStore } from '../../store/playerStore';
import { useLuckyMixStore } from '../../store/luckyMixStore';
import type { QueueItemRef, PlayerState } from '../../store/playerStoreTypes';
import type { QueueDisplayMode } from '../../store/authStoreTypes';
-import { formatTrackTime } from '../../utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import { resolveQueueTrack } from '../../utils/library/queueTrackView';
import {
getQueueResolverVersion,
diff --git a/src/config/shortcutActionRegistry.ts b/src/config/shortcutActionRegistry.ts
index cf999f52..a997cb34 100644
--- a/src/config/shortcutActionRegistry.ts
+++ b/src/config/shortcutActionRegistry.ts
@@ -3,7 +3,7 @@ import { getSong } from '@/lib/api/subsonicLibrary';
import { songToTrack } from '../utils/playback/songToTrack';
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
-import i18n from '../i18n';
+import i18n from '@/lib/i18n';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useLyricsStore } from '../store/lyricsStore';
diff --git a/src/config/shortcutDispatch.ts b/src/config/shortcutDispatch.ts
index aee97109..a3061d94 100644
--- a/src/config/shortcutDispatch.ts
+++ b/src/config/shortcutDispatch.ts
@@ -1,5 +1,5 @@
import { queueSongRating } from '../store/pendingStarSync';
-import i18n from '../i18n';
+import i18n from '@/lib/i18n';
import { usePlayerStore } from '../store/playerStore';
import { showToast } from '../utils/ui/toast';
import { playByOpaqueId } from '../utils/playback/playByOpaqueId';
diff --git a/src/features/album/components/AlbumHeader.tsx b/src/features/album/components/AlbumHeader.tsx
index 91256bbe..64d069f9 100644
--- a/src/features/album/components/AlbumHeader.tsx
+++ b/src/features/album/components/AlbumHeader.tsx
@@ -14,9 +14,9 @@ import StarRating from '@/components/StarRating';
import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink';
import { showToast } from '@/utils/ui/toast';
import { isAlbumRecentlyAdded } from '@/features/album/utils/albumRecency';
-import { formatLongDuration } from '@/utils/format/formatDuration';
-import { formatMb } from '@/utils/format/formatBytes';
-import { sanitizeHtml } from '@/utils/sanitizeHtml';
+import { formatLongDuration } from '@/lib/format/formatDuration';
+import { formatMb } from '@/lib/format/formatBytes';
+import { sanitizeHtml } from '@/lib/util/sanitizeHtml';
import { OpenArtistRefInline } from '@/features/artist';
import { tooltipAttrs } from '@/ui/tooltipAttrs';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
diff --git a/src/features/album/components/AlbumRow.tsx b/src/features/album/components/AlbumRow.tsx
index 77727796..65e34ece 100644
--- a/src/features/album/components/AlbumRow.tsx
+++ b/src/features/album/components/AlbumRow.tsx
@@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react';
import { NavLink, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
-import { dedupeById } from '@/utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
interface Props {
title: string;
diff --git a/src/features/album/components/AlbumTrackListMobile.tsx b/src/features/album/components/AlbumTrackListMobile.tsx
index b76df24d..be529dde 100644
--- a/src/features/album/components/AlbumTrackListMobile.tsx
+++ b/src/features/album/components/AlbumTrackListMobile.tsx
@@ -3,7 +3,7 @@ import { AudioLines } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { Track } from '@/store/playerStoreTypes';
import { songToTrack } from '@/utils/playback/songToTrack';
-import { formatLongDuration } from '@/utils/format/formatDuration';
+import { formatLongDuration } from '@/lib/format/formatDuration';
interface Props {
discNums: number[];
diff --git a/src/features/album/components/TrackRow.tsx b/src/features/album/components/TrackRow.tsx
index 99cd55e0..bd953939 100644
--- a/src/features/album/components/TrackRow.tsx
+++ b/src/features/album/components/TrackRow.tsx
@@ -11,9 +11,9 @@ import { useThemeStore } from '@/store/themeStore';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import StarRating from '@/components/StarRating';
import { codecLabel, type ColKey } from '@/features/album/utils/albumTrackListHelpers';
-import { formatLongDuration } from '@/utils/format/formatDuration';
+import { formatLongDuration } from '@/lib/format/formatDuration';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
-import i18n from '@/i18n';
+import i18n from '@/lib/i18n';
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
diff --git a/src/features/album/hooks/useAlbumBrowseData.ts b/src/features/album/hooks/useAlbumBrowseData.ts
index 8a00d468..19099b7d 100644
--- a/src/features/album/hooks/useAlbumBrowseData.ts
+++ b/src/features/album/hooks/useAlbumBrowseData.ts
@@ -12,7 +12,7 @@ import {
coverTrafficGridPaginationDepth,
} from '@/cover/coverTraffic';
import { coverEnsureQueueBacklog, coverEnsureResumePump, coverEnsureSubscribeBacklogDrain } from '@/cover/ensureQueue';
-import { dedupeById } from '@/utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import { albumBrowseCompScanComplete, albumBrowseCompFilterClientOnly } from '@/utils/library/albumCompilation';
import type { AlbumCompFilter } from '@/utils/library/albumCompilation';
import {
diff --git a/src/features/album/hooks/useGenreAlbumBrowse.ts b/src/features/album/hooks/useGenreAlbumBrowse.ts
index d305b0ae..8f6bbe7c 100644
--- a/src/features/album/hooks/useGenreAlbumBrowse.ts
+++ b/src/features/album/hooks/useGenreAlbumBrowse.ts
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
-import { dedupeById } from '@/utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import type { AlbumBrowseSort } from '@/utils/library/albumBrowseSort';
import {
fetchGenreAlbumPage,
diff --git a/src/features/album/pages/RandomAlbums.tsx b/src/features/album/pages/RandomAlbums.tsx
index f7fef95a..0a829222 100644
--- a/src/features/album/pages/RandomAlbums.tsx
+++ b/src/features/album/pages/RandomAlbums.tsx
@@ -3,7 +3,7 @@ import { getAlbumsByGenre } from '@/lib/api/subsonicGenres';
import { getAlbumList } from '@/lib/api/subsonicLibrary';
import { resolveAlbum } from '@/features/offline';
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
-import { dedupeById } from '@/utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import { shuffleArray } from '@/utils/playback/shuffleArray';
import React, { useEffect, useLayoutEffect, useState, useCallback, useRef } from 'react';
import { RefreshCw, Download, HardDriveDownload } from 'lucide-react';
diff --git a/src/features/artist/components/ArtistDetailTopTracks.tsx b/src/features/artist/components/ArtistDetailTopTracks.tsx
index cdd6e8ac..1cc4b221 100644
--- a/src/features/artist/components/ArtistDetailTopTracks.tsx
+++ b/src/features/artist/components/ArtistDetailTopTracks.tsx
@@ -6,7 +6,7 @@ import { usePlayerStore } from '@/store/playerStore';
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
import { useOrbitSongRowBehavior } from '@/features/orbit';
import { songToTrack } from '@/utils/playback/songToTrack';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import ArtistTopTrackCover from '@/features/artist/components/ArtistTopTrackCover';
import { topSongAlbumForCover } from '@/features/artist/components/topSongAlbumForCover';
diff --git a/src/features/artist/hooks/useArtistsBrowseCatalog.ts b/src/features/artist/hooks/useArtistsBrowseCatalog.ts
index 267174e8..c24d1344 100644
--- a/src/features/artist/hooks/useArtistsBrowseCatalog.ts
+++ b/src/features/artist/hooks/useArtistsBrowseCatalog.ts
@@ -1,7 +1,7 @@
import { getArtists } from '@/lib/api/subsonicArtists';
import type { SubsonicArtist } from '@/lib/api/subsonicTypes';
import { useCallback, useEffect, useRef, useState } from 'react';
-import { dedupeById } from '@/utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import {
fetchLocalArtistCatalogChunk,
fetchNetworkStarredArtists,
diff --git a/src/features/deviceSync/components/DeviceSyncPreSyncModal.tsx b/src/features/deviceSync/components/DeviceSyncPreSyncModal.tsx
index 6ffd5fd0..fbbd4f6c 100644
--- a/src/features/deviceSync/components/DeviceSyncPreSyncModal.tsx
+++ b/src/features/deviceSync/components/DeviceSyncPreSyncModal.tsx
@@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, Loader2 } from 'lucide-react';
import type { SyncDelta } from '@/features/deviceSync/utils/runDeviceSyncExecution';
-import { formatMb } from '@/utils/format/formatBytes';
+import { formatMb } from '@/lib/format/formatBytes';
interface Props {
preSyncOpen: boolean;
diff --git a/src/features/deviceSync/utils/deviceSyncLegacyTemplate.ts b/src/features/deviceSync/utils/deviceSyncLegacyTemplate.ts
index 23bfebf5..715537ec 100644
--- a/src/features/deviceSync/utils/deviceSyncLegacyTemplate.ts
+++ b/src/features/deviceSync/utils/deviceSyncLegacyTemplate.ts
@@ -1,4 +1,4 @@
-import { IS_WINDOWS } from '@/utils/platform';
+import { IS_WINDOWS } from '@/lib/util/platform';
// Same sanitize rules the Rust side uses (`sanitize_path_component`): strip
// Windows-illegal chars and control chars, trim leading/trailing dots + spaces.
diff --git a/src/features/deviceSync/utils/runDeviceSyncMigration.ts b/src/features/deviceSync/utils/runDeviceSyncMigration.ts
index 7652445e..103fec45 100644
--- a/src/features/deviceSync/utils/runDeviceSyncMigration.ts
+++ b/src/features/deviceSync/utils/runDeviceSyncMigration.ts
@@ -5,7 +5,7 @@ import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { applyLegacyTemplate } from '@/features/deviceSync/utils/deviceSyncLegacyTemplate';
import { trackToSyncInfo } from '@/features/deviceSync/utils/deviceSyncHelpers';
import { fetchTracksForSource } from '@/utils/playback/fetchTracksForSource';
-import { IS_WINDOWS } from '@/utils/platform';
+import { IS_WINDOWS } from '@/lib/util/platform';
export type MigrationPhase = 'closed' | 'loading' | 'preview' | 'executing' | 'done' | 'nothing';
diff --git a/src/features/favorites/components/FavoriteSongRow.tsx b/src/features/favorites/components/FavoriteSongRow.tsx
index 8ed192aa..0a534901 100644
--- a/src/features/favorites/components/FavoriteSongRow.tsx
+++ b/src/features/favorites/components/FavoriteSongRow.tsx
@@ -5,8 +5,8 @@ import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
-import i18n from '@/i18n';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import i18n from '@/lib/i18n';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import StarRating from '@/components/StarRating';
import { OpenArtistRefInline } from '@/features/artist';
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
diff --git a/src/features/fullscreenPlayer/components/FsClock.tsx b/src/features/fullscreenPlayer/components/FsClock.tsx
index 83f9ec6c..14478e4d 100644
--- a/src/features/fullscreenPlayer/components/FsClock.tsx
+++ b/src/features/fullscreenPlayer/components/FsClock.tsx
@@ -1,7 +1,7 @@
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/store/authStore';
-import { formatClockTime } from '@/utils/format/formatClockTime';
+import { formatClockTime } from '@/lib/format/formatClockTime';
/**
* Standalone wall-clock for the fullscreen player. Owns its own state so the
diff --git a/src/features/fullscreenPlayer/components/FsQueueModal.tsx b/src/features/fullscreenPlayer/components/FsQueueModal.tsx
index c3edfe66..95fcca40 100644
--- a/src/features/fullscreenPlayer/components/FsQueueModal.tsx
+++ b/src/features/fullscreenPlayer/components/FsQueueModal.tsx
@@ -8,7 +8,7 @@ import {
getQueueResolverVersion,
subscribeQueueResolver,
} from '@/utils/library/queueTrackResolver';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
interface Props {
onClose: () => void;
diff --git a/src/features/fullscreenPlayer/components/FsTimeReadout.tsx b/src/features/fullscreenPlayer/components/FsTimeReadout.tsx
index 4f9d8af1..14c39091 100644
--- a/src/features/fullscreenPlayer/components/FsTimeReadout.tsx
+++ b/src/features/fullscreenPlayer/components/FsTimeReadout.tsx
@@ -1,6 +1,6 @@
import { memo, useEffect, useRef } from 'react';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '@/store/playbackProgress';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
/**
* Centered "current / total" readout for the control bar. Updates the current
diff --git a/src/features/miniPlayer/components/MiniControls.tsx b/src/features/miniPlayer/components/MiniControls.tsx
index 5fa75c9e..562bf6d6 100644
--- a/src/features/miniPlayer/components/MiniControls.tsx
+++ b/src/features/miniPlayer/components/MiniControls.tsx
@@ -1,5 +1,5 @@
import { Pause, Play, SkipBack, SkipForward } from 'lucide-react';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import type { MiniControlAction } from '@/features/miniPlayer/utils/miniPlayerBridge';
interface Props {
diff --git a/src/features/miniPlayer/components/MiniTitlebar.tsx b/src/features/miniPlayer/components/MiniTitlebar.tsx
index a80b991d..8dce6f9d 100644
--- a/src/features/miniPlayer/components/MiniTitlebar.tsx
+++ b/src/features/miniPlayer/components/MiniTitlebar.tsx
@@ -1,6 +1,6 @@
import { Maximize2, Pin, PinOff, X } from 'lucide-react';
import type { TFunction } from 'i18next';
-import { IS_LINUX } from '@/utils/platform';
+import { IS_LINUX } from '@/lib/util/platform';
interface Props {
trackTitle: string | undefined;
diff --git a/src/features/miniPlayer/hooks/useMiniWindowSetup.ts b/src/features/miniPlayer/hooks/useMiniWindowSetup.ts
index 2b3b6bb3..14a32797 100644
--- a/src/features/miniPlayer/hooks/useMiniWindowSetup.ts
+++ b/src/features/miniPlayer/hooks/useMiniWindowSetup.ts
@@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '@/store/authStore';
-import { IS_LINUX } from '@/utils/platform';
+import { IS_LINUX } from '@/lib/util/platform';
import {
EXPANDED_SIZE, EXPANDED_MIN, readStoredExpandedHeight,
} from '@/features/miniPlayer/utils/miniPlayerHelpers';
diff --git a/src/features/nowPlaying/components/AlbumCard.tsx b/src/features/nowPlaying/components/AlbumCard.tsx
index f0b60279..6136ae37 100644
--- a/src/features/nowPlaying/components/AlbumCard.tsx
+++ b/src/features/nowPlaying/components/AlbumCard.tsx
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Disc3, ExternalLink, Star } from 'lucide-react';
import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
import { formatTotalDuration } from '@/utils/componentHelpers/nowPlayingHelpers';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
interface AlbumCardProps {
album: SubsonicAlbum | null;
diff --git a/src/features/nowPlaying/components/Hero.tsx b/src/features/nowPlaying/components/Hero.tsx
index 36995327..963c2b08 100644
--- a/src/features/nowPlaying/components/Hero.tsx
+++ b/src/features/nowPlaying/components/Hero.tsx
@@ -6,7 +6,7 @@ import type { CoverArtRef } from '@/cover/types';
import type { ArtistStats, TrackStats } from '@/music-network';
import type { SubsonicOpenArtistRef } from '@/lib/api/subsonicTypes';
import { OpenArtistRefInline } from '@/features/artist';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel';
import { useEnrichmentPrimaryIcon } from '@/hooks/useEnrichmentPrimaryIcon';
import { renderPresetIcon } from '@/components/settings/musicNetwork/presetIcon';
diff --git a/src/features/nowPlaying/components/RadioView.tsx b/src/features/nowPlaying/components/RadioView.tsx
index 84f06bfd..ee859f85 100644
--- a/src/features/nowPlaying/components/RadioView.tsx
+++ b/src/features/nowPlaying/components/RadioView.tsx
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Cast, Clock, Radio, SkipForward, Users } from 'lucide-react';
import type { useRadioMetadata } from '@/features/radio';
import { usePlayerStore } from '@/store/playerStore';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
type NonNullStoreField> =
NonNullable[K]>;
diff --git a/src/features/nowPlaying/components/TopSongsCard.tsx b/src/features/nowPlaying/components/TopSongsCard.tsx
index fbb45bfd..abbda127 100644
--- a/src/features/nowPlaying/components/TopSongsCard.tsx
+++ b/src/features/nowPlaying/components/TopSongsCard.tsx
@@ -2,7 +2,7 @@ import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { ExternalLink, Play, TrendingUp } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
interface TopSongsCardProps {
artistName: string;
diff --git a/src/features/offline/components/OfflineLibraryDiskStat.tsx b/src/features/offline/components/OfflineLibraryDiskStat.tsx
index 25dc3d60..50ecb439 100644
--- a/src/features/offline/components/OfflineLibraryDiskStat.tsx
+++ b/src/features/offline/components/OfflineLibraryDiskStat.tsx
@@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { HardDriveDownload, Heart } from 'lucide-react';
import { useTranslation } from 'react-i18next';
-import { formatBytes } from '@/utils/format/formatBytes';
+import { formatBytes } from '@/lib/format/formatBytes';
const OPEN_DELAY_MS = 450;
diff --git a/src/features/offline/components/ZipDownloadOverlay.tsx b/src/features/offline/components/ZipDownloadOverlay.tsx
index 8613d45d..c03586fd 100644
--- a/src/features/offline/components/ZipDownloadOverlay.tsx
+++ b/src/features/offline/components/ZipDownloadOverlay.tsx
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { createPortal } from 'react-dom';
import { HardDriveDownload, Check, X } from 'lucide-react';
import { useZipDownloadStore } from '@/features/offline/store/zipDownloadStore';
-import { formatBytes } from '@/utils/format/formatBytes';
+import { formatBytes } from '@/lib/format/formatBytes';
function ZipDownloadItem({ id }: { id: string }) {
const dismiss = useZipDownloadStore(s => s.dismiss);
diff --git a/src/features/offline/utils/favoritesOfflineSync.ts b/src/features/offline/utils/favoritesOfflineSync.ts
index ca494d91..1d86c619 100644
--- a/src/features/offline/utils/favoritesOfflineSync.ts
+++ b/src/features/offline/utils/favoritesOfflineSync.ts
@@ -6,7 +6,7 @@ import { getStarredForServer } from '@/lib/api/subsonicStarRating';
import { buildStreamUrlForServer } from '@/lib/api/subsonicStreamUrl';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { invoke } from '@tauri-apps/api/core';
-import i18n from '@/i18n';
+import i18n from '@/lib/i18n';
import { useAuthStore } from '@/store/authStore';
import { cancelledDownloads, useOfflineJobStore } from '@/features/offline/store/offlineJobStore';
import { useFavoritesOfflineSyncStore } from '@/features/offline/store/favoritesOfflineSyncStore';
diff --git a/src/features/offline/utils/offlineStarredLoad.ts b/src/features/offline/utils/offlineStarredLoad.ts
index dedcaa00..7abb10a4 100644
--- a/src/features/offline/utils/offlineStarredLoad.ts
+++ b/src/features/offline/utils/offlineStarredLoad.ts
@@ -11,7 +11,7 @@ import {
albumToAlbum,
trackToSong,
} from '@/utils/library/advancedSearchLocal';
-import { dedupeById } from '@/utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
import { favoritesServerIds } from '@/features/offline/utils/favoritesOfflineBrowse';
import {
diff --git a/src/features/orbit/components/OrbitSessionBar.tsx b/src/features/orbit/components/OrbitSessionBar.tsx
index c138866f..834a60c8 100644
--- a/src/features/orbit/components/OrbitSessionBar.tsx
+++ b/src/features/orbit/components/OrbitSessionBar.tsx
@@ -19,7 +19,7 @@ import OrbitSettingsPopover from '@/features/orbit/components/OrbitSettingsPopov
import OrbitSharePopover from '@/features/orbit/components/OrbitSharePopover';
import OrbitDiagnosticsPopover from '@/features/orbit/components/OrbitDiagnosticsPopover';
import ConfirmModal from '@/components/ConfirmModal';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
/**
* Orbit — top-strip session indicator.
diff --git a/src/features/orbit/hooks/useOrbitGuest.ts b/src/features/orbit/hooks/useOrbitGuest.ts
index 61531106..5667d494 100644
--- a/src/features/orbit/hooks/useOrbitGuest.ts
+++ b/src/features/orbit/hooks/useOrbitGuest.ts
@@ -15,7 +15,7 @@ import {
resetPendingResendState,
} from '@/features/orbit/utils/orbit';
import { showToast } from '@/utils/ui/toast';
-import i18n from '@/i18n';
+import i18n from '@/lib/i18n';
import { estimateLivePosition, type OrbitState } from '@/features/orbit/api/orbit';
import { pushOrbitEvent } from '@/features/orbit/utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from '@/features/orbit/hooks/useOrbitOutboxHeartbeat';
diff --git a/src/features/orbit/hooks/useOrbitHost.ts b/src/features/orbit/hooks/useOrbitHost.ts
index f4bb4195..d404ae5a 100644
--- a/src/features/orbit/hooks/useOrbitHost.ts
+++ b/src/features/orbit/hooks/useOrbitHost.ts
@@ -20,7 +20,7 @@ import {
type OrbitQueueItem,
} from '@/features/orbit/api/orbit';
import { showToast } from '@/utils/ui/toast';
-import i18n from '@/i18n';
+import i18n from '@/lib/i18n';
import { pushOrbitEvent } from '@/features/orbit/utils/orbitDiag';
import { useOrbitOutboxHeartbeat } from '@/features/orbit/hooks/useOrbitOutboxHeartbeat';
diff --git a/src/features/orbit/utils/orbitBulkGuard.ts b/src/features/orbit/utils/orbitBulkGuard.ts
index fa5517e6..4774762c 100644
--- a/src/features/orbit/utils/orbitBulkGuard.ts
+++ b/src/features/orbit/utils/orbitBulkGuard.ts
@@ -1,6 +1,6 @@
import { useOrbitStore } from '@/features/orbit/store/orbitStore';
import { useConfirmModalStore } from '@/store/confirmModalStore';
-import i18n from '@/i18n';
+import i18n from '@/lib/i18n';
/**
* Ask the user before dropping many tracks into the shared Orbit queue.
diff --git a/src/features/playlist/components/PlaylistCard.tsx b/src/features/playlist/components/PlaylistCard.tsx
index 8475c22a..4cbec304 100644
--- a/src/features/playlist/components/PlaylistCard.tsx
+++ b/src/features/playlist/components/PlaylistCard.tsx
@@ -7,7 +7,7 @@ import { usePlayerStore } from '@/store/playerStore';
import {
displayPlaylistName, isSmartPlaylistName, type PendingSmartPlaylist,
} from '@/features/playlist/utils/playlistsSmart';
-import { formatHumanHoursMinutes } from '@/utils/format/formatHumanDuration';
+import { formatHumanHoursMinutes } from '@/lib/format/formatHumanDuration';
import { useDragSource } from '@/contexts/DragDropContext';
import { PlaylistCardMainCover, PlaylistSmartCoverCell } from '@/features/playlist/components/PlaylistCoverImages';
diff --git a/src/features/playlist/components/PlaylistRow.tsx b/src/features/playlist/components/PlaylistRow.tsx
index c96bb48b..1403eacd 100644
--- a/src/features/playlist/components/PlaylistRow.tsx
+++ b/src/features/playlist/components/PlaylistRow.tsx
@@ -5,8 +5,8 @@ import type { ColDef } from '@/utils/useTracklistColumns';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
-import i18n from '@/i18n';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import i18n from '@/lib/i18n';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import StarRating from '@/components/StarRating';
import { PlaylistArtistCell } from '@/features/playlist/components/PlaylistArtistCell';
diff --git a/src/features/playlist/components/PlaylistSongSearchPanel.tsx b/src/features/playlist/components/PlaylistSongSearchPanel.tsx
index 33533c3d..35295f8a 100644
--- a/src/features/playlist/components/PlaylistSongSearchPanel.tsx
+++ b/src/features/playlist/components/PlaylistSongSearchPanel.tsx
@@ -4,7 +4,7 @@ import { Check, ListPlus, X } from 'lucide-react';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import { usePlayerStore } from '@/store/playerStore';
import { songToTrack } from '@/utils/playback/songToTrack';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
import { AddToPlaylistSubmenu } from '@/components/ContextMenu';
diff --git a/src/features/playlist/components/PlaylistSuggestions.tsx b/src/features/playlist/components/PlaylistSuggestions.tsx
index 4e26f4c9..3e473842 100644
--- a/src/features/playlist/components/PlaylistSuggestions.tsx
+++ b/src/features/playlist/components/PlaylistSuggestions.tsx
@@ -14,8 +14,8 @@ import { songToTrack } from '@/utils/playback/songToTrack';
import { getQueueTracksView } from '@/utils/library/queueTrackView';
import { codecLabel } from '@/utils/componentHelpers/playlistDetailHelpers';
import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
-import { formatTrackTime } from '@/utils/format/formatDuration';
-import i18n from '@/i18n';
+import { formatTrackTime } from '@/lib/format/formatDuration';
+import i18n from '@/lib/i18n';
const PL_CENTERED = new Set(['favorite', 'rating', 'duration', 'playCount', 'bpm']);
diff --git a/src/features/search/components/ShareQueuePreviewModal.tsx b/src/features/search/components/ShareQueuePreviewModal.tsx
index ab2f2019..5c5ccccf 100644
--- a/src/features/search/components/ShareQueuePreviewModal.tsx
+++ b/src/features/search/components/ShareQueuePreviewModal.tsx
@@ -4,7 +4,7 @@ import { Music, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
import type { ServerProfile } from '@/store/authStoreTypes';
-import { formatTrackTime } from '@/utils/format/formatDuration';
+import { formatTrackTime } from '@/lib/format/formatDuration';
import type { ShareQueuePreviewState } from '@/features/search/hooks/useShareQueuePreview';
import { sharePayloadTotal, type QueueableShareSearchPayload } from '@/utils/share/shareSearch';
import OverlayScrollArea from '@/ui/OverlayScrollArea';
diff --git a/src/features/settings/components/AppearanceTab.tsx b/src/features/settings/components/AppearanceTab.tsx
index 40e60668..d701bd03 100644
--- a/src/features/settings/components/AppearanceTab.tsx
+++ b/src/features/settings/components/AppearanceTab.tsx
@@ -10,7 +10,7 @@ import {
import type { SeekbarStyle, WindowButtonStyle } from '@/store/authStoreTypes';
import { useFontStore, FontId } from '@/store/fontStore';
import { useThemeStore } from '@/store/themeStore';
-import { IS_LINUX, IS_WINDOWS } from '@/utils/platform';
+import { IS_LINUX, IS_WINDOWS } from '@/lib/util/platform';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
diff --git a/src/features/settings/components/AudioTab.tsx b/src/features/settings/components/AudioTab.tsx
index 78dbe8b9..22fb310f 100644
--- a/src/features/settings/components/AudioTab.tsx
+++ b/src/features/settings/components/AudioTab.tsx
@@ -8,7 +8,7 @@ import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '@/utils/audio/loudnessPreAnalysisSlider';
import { useAudioDevicesProbe } from '@/hooks/useAudioDevicesProbe';
-import { IS_MACOS } from '@/utils/platform';
+import { IS_MACOS } from '@/lib/util/platform';
import { AudioOutputDeviceSection } from '@/features/settings/components/audio/AudioOutputDeviceSection';
import { NormalizationBlock } from '@/features/settings/components/audio/NormalizationBlock';
import { PlaybackRateBlock } from '@/features/settings/components/audio/PlaybackRateBlock';
diff --git a/src/features/settings/components/CoverCacheStrategySection.tsx b/src/features/settings/components/CoverCacheStrategySection.tsx
index bea3adab..b136d832 100644
--- a/src/features/settings/components/CoverCacheStrategySection.tsx
+++ b/src/features/settings/components/CoverCacheStrategySection.tsx
@@ -17,7 +17,7 @@ import { clearDiskSrcCacheForServer } from '@/cover/diskSrcCache';
import { serverListDisplayLabel } from '@/utils/server/serverDisplayName';
import { serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { showToast } from '@/utils/ui/toast';
-import { formatBytes } from '@/utils/format/formatBytes';
+import { formatBytes } from '@/lib/format/formatBytes';
import { clearImageCache, getImageCacheSize } from '@/utils/imageCache';
import { wakeLibraryCoverBackfill } from '@/utils/library/coverBackfillWake';
import {
diff --git a/src/features/settings/components/StorageTab.tsx b/src/features/settings/components/StorageTab.tsx
index dea83f6a..5d8bef34 100644
--- a/src/features/settings/components/StorageTab.tsx
+++ b/src/features/settings/components/StorageTab.tsx
@@ -6,7 +6,7 @@ import { Download, FolderOpen, Trash2, X } from 'lucide-react';
import { useAuthStore } from '@/store/authStore';
import { countHotCacheTracks } from '@/store/hotCacheStore';
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
-import { formatBytes, snapHotCacheMb } from '@/utils/format/formatBytes';
+import { formatBytes, snapHotCacheMb } from '@/lib/format/formatBytes';
import SettingsSubSection from '@/features/settings/components/SettingsSubSection';
import { SettingsGroup } from '@/features/settings/components/SettingsGroup';
import { SettingsToggle } from '@/features/settings/components/SettingsToggle';
diff --git a/src/features/settings/components/SystemTab.tsx b/src/features/settings/components/SystemTab.tsx
index 7d2738cd..50f925c1 100644
--- a/src/features/settings/components/SystemTab.tsx
+++ b/src/features/settings/components/SystemTab.tsx
@@ -6,10 +6,10 @@ import { save as saveDialog } from '@tauri-apps/plugin-dialog';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import { AppWindow, ChevronDown, Download, ExternalLink, Globe, HardDrive, Info, Scale, Sliders, Users } from 'lucide-react';
import { version as appVersion } from '@/../package.json';
-import i18n from '@/i18n';
+import i18n from '@/lib/i18n';
import { useAuthStore } from '@/store/authStore';
import type { ClockFormat, LinuxWaylandTextRenderProfile, LoggingMode } from '@/store/authStoreTypes';
-import { IS_LINUX } from '@/utils/platform';
+import { IS_LINUX } from '@/lib/util/platform';
import { showToast } from '@/utils/ui/toast';
import { AboutPsysonicBrandHeader } from '@/components/AboutPsysonicLol';
import CustomSelect from '@/ui/CustomSelect';
diff --git a/src/features/settings/components/ThemeStoreSection.tsx b/src/features/settings/components/ThemeStoreSection.tsx
index 7b57995f..8aa55cbf 100644
--- a/src/features/settings/components/ThemeStoreSection.tsx
+++ b/src/features/settings/components/ThemeStoreSection.tsx
@@ -6,7 +6,7 @@ import CoverLightbox from '@/components/CoverLightbox';
import { useThemeAnimationRisk } from '@/hooks/useThemeAnimationRisk';
import { AnimatedThemeBadge } from '@/features/settings/components/AnimatedThemeBadge';
import CustomSelect from '@/ui/CustomSelect';
-import { formatRelativeTime } from '@/utils/format/relativeTime';
+import { formatRelativeTime } from '@/lib/format/relativeTime';
import { useThemeStore } from '@/store/themeStore';
import { useInstalledThemesStore, type InstalledTheme } from '@/store/installedThemesStore';
import {
diff --git a/src/features/settings/pages/Settings.tsx b/src/features/settings/pages/Settings.tsx
index 781376fe..17e7a2c8 100644
--- a/src/features/settings/pages/Settings.tsx
+++ b/src/features/settings/pages/Settings.tsx
@@ -5,7 +5,7 @@ import {
AudioLines, HardDrive, Info, Keyboard, LayoutGrid, Music2, Paintbrush, Palette, Search, Server, Sparkles, Users, X,
} from 'lucide-react';
import { useAuthStore } from '@/store/authStore';
-import { IS_MACOS } from '@/utils/platform';
+import { IS_MACOS } from '@/lib/util/platform';
import { AppearanceTab } from '@/features/settings/components/AppearanceTab';
import { ThemesTab } from '@/features/settings/components/ThemesTab';
import { AudioTab } from '@/features/settings/components/AudioTab';
diff --git a/src/features/stats/components/PlayerStatisticsPanel.tsx b/src/features/stats/components/PlayerStatisticsPanel.tsx
index f793f4cc..cba4d315 100644
--- a/src/features/stats/components/PlayerStatisticsPanel.tsx
+++ b/src/features/stats/components/PlayerStatisticsPanel.tsx
@@ -13,7 +13,7 @@ import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayer
import PlayerStatsHeatmap from '@/features/stats/components/PlayerStatsHeatmap';
import PlayerStatsIndexRequiredNotice from '@/features/stats/components/PlayerStatsIndexRequiredNotice';
import PlayerStatsRecentDays from '@/features/stats/components/PlayerStatsRecentDays';
-import { formatPlayerStatsListeningTotal } from '@/utils/format/formatHumanDuration';
+import { formatPlayerStatsListeningTotal } from '@/lib/format/formatHumanDuration';
const currentCalendarYear = () => new Date().getFullYear();
diff --git a/src/features/stats/components/PlayerStatsDayTracks.tsx b/src/features/stats/components/PlayerStatsDayTracks.tsx
index e8b2a879..36d7145e 100644
--- a/src/features/stats/components/PlayerStatsDayTracks.tsx
+++ b/src/features/stats/components/PlayerStatsDayTracks.tsx
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next';
import type { PlaySessionDayDetail } from '@/lib/api/library';
-import { formatPlayerStatsListenedSec } from '@/utils/format/formatHumanDuration';
+import { formatPlayerStatsListenedSec } from '@/lib/format/formatHumanDuration';
type Props = {
detail: PlaySessionDayDetail;
diff --git a/src/features/stats/components/PlayerStatsRecentDays.tsx b/src/features/stats/components/PlayerStatsRecentDays.tsx
index 550ec774..3fd01cac 100644
--- a/src/features/stats/components/PlayerStatsRecentDays.tsx
+++ b/src/features/stats/components/PlayerStatsRecentDays.tsx
@@ -7,7 +7,7 @@ import {
type PlaySessionDayDetail,
type PlaySessionRecentDay,
} from '@/lib/api/library';
-import { formatPlayerStatsListeningTotal } from '@/utils/format/formatHumanDuration';
+import { formatPlayerStatsListeningTotal } from '@/lib/format/formatHumanDuration';
import {
formatPlayerStatsDayLabel,
PLAYER_STATS_RECENT_DAYS_LIMIT,
diff --git a/src/features/stats/pages/Statistics.tsx b/src/features/stats/pages/Statistics.tsx
index f5931bd8..a3bc3ca5 100644
--- a/src/features/stats/pages/Statistics.tsx
+++ b/src/features/stats/pages/Statistics.tsx
@@ -4,7 +4,7 @@ import type { SubsonicAlbum, SubsonicGenre } from '@/lib/api/subsonicTypes';
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Share2 } from 'lucide-react';
-import { formatHumanHoursMinutes } from '@/utils/format/formatHumanDuration';
+import { formatHumanHoursMinutes } from '@/lib/format/formatHumanDuration';
import { AlbumRow } from '@/features/album';
import StatsExportModal from '@/features/stats/components/StatsExportModal';
import PlayerStatisticsPanel from '@/features/stats/components/PlayerStatisticsPanel';
diff --git a/src/hooks/tauriBridge/useCliBridge.ts b/src/hooks/tauriBridge/useCliBridge.ts
index e26411b4..449dafa8 100644
--- a/src/hooks/tauriBridge/useCliBridge.ts
+++ b/src/hooks/tauriBridge/useCliBridge.ts
@@ -10,7 +10,7 @@ import { shuffleArray } from '../../utils/playback/shuffleArray';
import { songToTrack } from '../../utils/playback/songToTrack';
import { showToast } from '../../utils/ui/toast';
import { switchActiveServer } from '../../utils/server/switchActiveServer';
-import i18n from '../../i18n';
+import i18n from '@/lib/i18n';
import { usePlayerStore } from '../../store/playerStore';
import { useAuthStore } from '../../store/authStore';
import { executeCliPlayerCommand } from '../../config/shortcutActions';
diff --git a/src/hooks/useAppUpdater.ts b/src/hooks/useAppUpdater.ts
index 87f8a1d7..cbea8005 100644
--- a/src/hooks/useAppUpdater.ts
+++ b/src/hooks/useAppUpdater.ts
@@ -4,7 +4,7 @@ import { dirname } from '@tauri-apps/api/path';
import { invoke } from '@tauri-apps/api/core';
import { useTranslation } from 'react-i18next';
import { version as currentVersion } from '../../package.json';
-import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform';
+import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '@/lib/util/platform';
import { SKIP_KEY, isNewer, isWithinModerationWindow, pickAsset, type ReleaseData, type DlState } from '../utils/componentHelpers/appUpdaterHelpers';
/** All update-modal state, the GitHub release probe and the download/relaunch
diff --git a/src/hooks/useAudioDevicesProbe.ts b/src/hooks/useAudioDevicesProbe.ts
index f601c790..8f6c2bf1 100644
--- a/src/hooks/useAudioDevicesProbe.ts
+++ b/src/hooks/useAudioDevicesProbe.ts
@@ -3,7 +3,7 @@ import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import type { TFunction } from 'i18next';
import { useAuthStore } from '../store/authStore';
-import { IS_MACOS } from '../utils/platform';
+import { IS_MACOS } from '@/lib/util/platform';
import { sortAudioDeviceIds } from '../utils/audio/audioDeviceLabels';
import { showToast } from '../utils/ui/toast';
diff --git a/src/hooks/usePlatformShellSetup.ts b/src/hooks/usePlatformShellSetup.ts
index 80801809..e70bed91 100644
--- a/src/hooks/usePlatformShellSetup.ts
+++ b/src/hooks/usePlatformShellSetup.ts
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '../store/authStore';
import type { LinuxWaylandTextRenderProfile } from '../store/authStoreTypes';
-import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../utils/platform';
+import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '@/lib/util/platform';
/**
* One-shot platform + window-shell configuration. Reads tiling-WM state,
diff --git a/src/hooks/useThemeAnimationRisk.ts b/src/hooks/useThemeAnimationRisk.ts
index ba11f65d..7f124aef 100644
--- a/src/hooks/useThemeAnimationRisk.ts
+++ b/src/hooks/useThemeAnimationRisk.ts
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
-import { IS_LINUX } from '../utils/platform';
+import { IS_LINUX } from '@/lib/util/platform';
/**
* Whether animated themes may raise CPU load on this setup — Linux with the
diff --git a/src/utils/format/formatBytes.ts b/src/lib/format/formatBytes.ts
similarity index 100%
rename from src/utils/format/formatBytes.ts
rename to src/lib/format/formatBytes.ts
diff --git a/src/utils/format/formatClockTime.test.ts b/src/lib/format/formatClockTime.test.ts
similarity index 95%
rename from src/utils/format/formatClockTime.test.ts
rename to src/lib/format/formatClockTime.test.ts
index baa41167..a5567e02 100644
--- a/src/utils/format/formatClockTime.test.ts
+++ b/src/lib/format/formatClockTime.test.ts
@@ -5,7 +5,7 @@
* tests pin only the `hour12` mapping, which is what the setting controls.
*/
import { describe, expect, it } from 'vitest';
-import { formatClockTime } from './formatClockTime';
+import { formatClockTime } from '@/lib/format/formatClockTime';
const SAMPLE_TS = Date.UTC(2026, 0, 1, 19, 17, 0); // 19:17 UTC, deterministic
diff --git a/src/utils/format/formatClockTime.ts b/src/lib/format/formatClockTime.ts
similarity index 91%
rename from src/utils/format/formatClockTime.ts
rename to src/lib/format/formatClockTime.ts
index 41eb9672..72c892d2 100644
--- a/src/utils/format/formatClockTime.ts
+++ b/src/lib/format/formatClockTime.ts
@@ -1,4 +1,4 @@
-import type { ClockFormat } from '../../store/authStoreTypes';
+import type { ClockFormat } from '@/store/authStoreTypes';
/**
* Localized wall-clock `HH:MM` for a timestamp (sleep-timer / queue-ETA labels).
diff --git a/src/utils/format/formatDuration.test.ts b/src/lib/format/formatDuration.test.ts
similarity index 98%
rename from src/utils/format/formatDuration.test.ts
rename to src/lib/format/formatDuration.test.ts
index 2c009bd5..21b1702b 100644
--- a/src/utils/format/formatDuration.test.ts
+++ b/src/lib/format/formatDuration.test.ts
@@ -6,7 +6,7 @@ import {
formatPlaybarToggleClock,
playbarMinuteFieldWidth,
PLAYBAR_CLOCK_PAD,
-} from './formatDuration';
+} from '@/lib/format/formatDuration';
describe('formatTrackTime', () => {
it('formats m:ss with zero-padded seconds', () => {
diff --git a/src/utils/format/formatDuration.ts b/src/lib/format/formatDuration.ts
similarity index 100%
rename from src/utils/format/formatDuration.ts
rename to src/lib/format/formatDuration.ts
diff --git a/src/utils/format/formatHumanDuration.test.ts b/src/lib/format/formatHumanDuration.test.ts
similarity index 96%
rename from src/utils/format/formatHumanDuration.test.ts
rename to src/lib/format/formatHumanDuration.test.ts
index 8d98b8d2..579c06ab 100644
--- a/src/utils/format/formatHumanDuration.test.ts
+++ b/src/lib/format/formatHumanDuration.test.ts
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest';
// Echo the i18n key + params so the rounding/branching can be asserted
// without depending on a locale's exact "N hours M minutes" template.
-vi.mock('../../i18n', () => ({
+vi.mock('@/lib/i18n', () => ({
default: {
t: (key: string, params: Record) => `${key}|${JSON.stringify(params)}`,
resolvedLanguage: 'en',
@@ -10,7 +10,7 @@ vi.mock('../../i18n', () => ({
},
}));
-import { formatHumanHoursMinutes, formatPlayerStatsListeningTotal, formatPlayerStatsListenedSec } from './formatHumanDuration';
+import { formatHumanHoursMinutes, formatPlayerStatsListeningTotal, formatPlayerStatsListenedSec } from '@/lib/format/formatHumanDuration';
describe('formatHumanHoursMinutes', () => {
it('rounds to the nearest minute instead of truncating', () => {
diff --git a/src/utils/format/formatHumanDuration.ts b/src/lib/format/formatHumanDuration.ts
similarity index 98%
rename from src/utils/format/formatHumanDuration.ts
rename to src/lib/format/formatHumanDuration.ts
index d3321dec..7d6e3103 100644
--- a/src/utils/format/formatHumanDuration.ts
+++ b/src/lib/format/formatHumanDuration.ts
@@ -1,4 +1,4 @@
-import i18n from '../../i18n';
+import i18n from '@/lib/i18n';
/**
* Totals / statistics: localized "N hours M minutes" (not track mm:ss).
diff --git a/src/utils/format/relativeTime.test.ts b/src/lib/format/relativeTime.test.ts
similarity index 93%
rename from src/utils/format/relativeTime.test.ts
rename to src/lib/format/relativeTime.test.ts
index 335707e1..47264410 100644
--- a/src/utils/format/relativeTime.test.ts
+++ b/src/lib/format/relativeTime.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { formatRelativeTime } from './relativeTime';
+import { formatRelativeTime } from '@/lib/format/relativeTime';
describe('formatRelativeTime', () => {
const SECOND = 1000;
diff --git a/src/utils/format/relativeTime.ts b/src/lib/format/relativeTime.ts
similarity index 100%
rename from src/utils/format/relativeTime.ts
rename to src/lib/format/relativeTime.ts
diff --git a/src/i18n.ts b/src/lib/i18n.ts
similarity index 63%
rename from src/i18n.ts
rename to src/lib/i18n.ts
index 808844d0..93ca03bf 100644
--- a/src/i18n.ts
+++ b/src/lib/i18n.ts
@@ -1,17 +1,17 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
-import { deTranslation } from './locales/de';
-import { enTranslation } from './locales/en';
-import { frTranslation } from './locales/fr';
-import { zhTranslation } from './locales/zh';
-import { nbTranslation } from './locales/nb';
-import { ruTranslation } from './locales/ru';
-import { nlTranslation } from './locales/nl';
-import { esTranslation } from './locales/es';
-import { roTranslation } from './locales/ro';
-import { jaTranslation } from './locales/ja';
-import { huTranslation } from './locales/hu';
-import { plTranslation } from './locales/pl';
+import { deTranslation } from '@/locales/de';
+import { enTranslation } from '@/locales/en';
+import { frTranslation } from '@/locales/fr';
+import { zhTranslation } from '@/locales/zh';
+import { nbTranslation } from '@/locales/nb';
+import { ruTranslation } from '@/locales/ru';
+import { nlTranslation } from '@/locales/nl';
+import { esTranslation } from '@/locales/es';
+import { roTranslation } from '@/locales/ro';
+import { jaTranslation } from '@/locales/ja';
+import { huTranslation } from '@/locales/hu';
+import { plTranslation } from '@/locales/pl';
const savedLanguage = localStorage.getItem('psysonic_language') || 'en';
diff --git a/src/utils/dedupeById.ts b/src/lib/util/dedupeById.ts
similarity index 100%
rename from src/utils/dedupeById.ts
rename to src/lib/util/dedupeById.ts
diff --git a/src/utils/platform.ts b/src/lib/util/platform.ts
similarity index 100%
rename from src/utils/platform.ts
rename to src/lib/util/platform.ts
diff --git a/src/store/safeStorage.test.ts b/src/lib/util/safeStorage.test.ts
similarity index 96%
rename from src/store/safeStorage.test.ts
rename to src/lib/util/safeStorage.test.ts
index 19ac5443..5c863043 100644
--- a/src/store/safeStorage.test.ts
+++ b/src/lib/util/safeStorage.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
-import { createSafeJSONStorage } from './safeStorage';
+import { createSafeJSONStorage } from '@/lib/util/safeStorage';
describe('createSafeJSONStorage', () => {
afterEach(() => {
diff --git a/src/store/safeStorage.ts b/src/lib/util/safeStorage.ts
similarity index 100%
rename from src/store/safeStorage.ts
rename to src/lib/util/safeStorage.ts
diff --git a/src/utils/sanitizeHtml.ts b/src/lib/util/sanitizeHtml.ts
similarity index 100%
rename from src/utils/sanitizeHtml.ts
rename to src/lib/util/sanitizeHtml.ts
diff --git a/src/main.tsx b/src/main.tsx
index b1889cfd..6087111d 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom/client';
import App from './App';
import { runPreReactBootstrap } from './app/bootstrap';
import { scheduleStartupSplashDismiss } from './app/startupSplash';
-import './i18n';
+import '@/lib/i18n';
import './styles/themes/index.css';
import './styles/layout/index.css';
import './styles/components/index.css';
diff --git a/src/pages/ComposerDetail.tsx b/src/pages/ComposerDetail.tsx
index 71a2f7d7..65ca2c3f 100644
--- a/src/pages/ComposerDetail.tsx
+++ b/src/pages/ComposerDetail.tsx
@@ -16,7 +16,7 @@ import { useAuthStore } from '../store/authStore';
import { useTranslation } from 'react-i18next';
import { copyEntityShareLink } from '../utils/share/copyEntityShareLink';
import { showToast } from '../utils/ui/toast';
-import { sanitizeHtml } from '../utils/sanitizeHtml';
+import { sanitizeHtml } from '@/lib/util/sanitizeHtml';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { albumGridWarmCovers } from '../cover/layoutSizes';
import { VirtualCardGrid } from '../components/VirtualCardGrid';
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index 7354923e..3c97deb0 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -16,7 +16,7 @@ import { useAuthStore } from '../store/authStore';
import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../utils/mix/mixRatingFilter';
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
import { bumpPerfCounter } from '../utils/perf/perfTelemetry';
-import { dedupeById } from '../utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import { shuffleArray } from '../utils/playback/shuffleArray';
import { useLibraryCoverPrefetch } from '../cover/useLibraryCoverPrefetch';
import { primeAlbumCoversForDisplay, warmHomeMainstageCovers } from '../cover/warmDiskPeek';
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx
index 72ef970b..a6649d29 100644
--- a/src/pages/Login.tsx
+++ b/src/pages/Login.tsx
@@ -12,7 +12,7 @@ import {
} from '../utils/server/serverHttpHeaders';
import { syncServerHttpContextForProfile } from '../utils/server/syncServerHttpContext';
import { useTranslation } from 'react-i18next';
-import i18n from '../i18n';
+import i18n from '@/lib/i18n';
import CustomSelect from '@/ui/CustomSelect';
import {
decodeServerMagicString,
diff --git a/src/pages/NewReleases.tsx b/src/pages/NewReleases.tsx
index aa43ec31..a1575341 100644
--- a/src/pages/NewReleases.tsx
+++ b/src/pages/NewReleases.tsx
@@ -3,7 +3,7 @@ import { getAlbumsByGenre } from '@/lib/api/subsonicGenres';
import { getAlbumList } from '@/lib/api/subsonicLibrary';
import { resolveAlbum } from '@/features/offline';
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
-import { dedupeById } from '../utils/dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import { useEffect, useLayoutEffect, useState, useCallback, useRef, useMemo } from 'react';
import { Download, HardDriveDownload } from 'lucide-react';
import SelectionToggleButton from '../components/SelectionToggleButton';
diff --git a/src/store/authStoreRehydrate.ts b/src/store/authStoreRehydrate.ts
index 8152957f..f80428f4 100644
--- a/src/store/authStoreRehydrate.ts
+++ b/src/store/authStoreRehydrate.ts
@@ -1,4 +1,4 @@
-import { IS_LINUX } from '../utils/platform';
+import { IS_LINUX } from '@/lib/util/platform';
import { sanitizeHiResCrossfadeResampleHz } from '../utils/audio/hiResCrossfadeResample';
import {
sanitizeAutodjOverlapCapMode,
diff --git a/src/store/miscActions.ts b/src/store/miscActions.ts
index 26e2da86..be4ea353 100644
--- a/src/store/miscActions.ts
+++ b/src/store/miscActions.ts
@@ -1,6 +1,6 @@
import { applyServerPlayQueue } from './applyServerPlayQueue';
import { invoke } from '@tauri-apps/api/core';
-import i18n from '../i18n';
+import i18n from '@/lib/i18n';
import { showToast } from '../utils/ui/toast';
import { useAuthStore } from './authStore';
import {
diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts
index 6ea716a6..a4700e64 100644
--- a/src/store/playerStore.ts
+++ b/src/store/playerStore.ts
@@ -2,7 +2,7 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { readInitialNetworkLovedCache, persistNetworkLovedCache } from './networkLovedCacheStorage';
import { readInitialPlayerPrefs, persistPlayerPrefs } from './playerPrefsStorage';
-import { createHydrationGatedStorage, createSafeJSONStorage } from './safeStorage';
+import { createHydrationGatedStorage, createSafeJSONStorage } from '@/lib/util/safeStorage';
import { emitPlaybackProgress } from './playbackProgress';
import type { PlayerState, QueueItemRef, Track } from './playerStoreTypes';
import { toQueueItemRefs } from '../utils/library/queueItemRef';
diff --git a/src/test/helpers/renderWithProviders.tsx b/src/test/helpers/renderWithProviders.tsx
index 3b8a54c6..770ec39e 100644
--- a/src/test/helpers/renderWithProviders.tsx
+++ b/src/test/helpers/renderWithProviders.tsx
@@ -16,7 +16,7 @@ import type { ReactElement, ReactNode } from 'react';
import { render, type RenderOptions, type RenderResult } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
-import i18n from '@/i18n';
+import i18n from '@/lib/i18n';
interface WrapperOptions {
route?: string;
diff --git a/src/utils/componentHelpers/appUpdaterHelpers.ts b/src/utils/componentHelpers/appUpdaterHelpers.ts
index 490d428c..b1589212 100644
--- a/src/utils/componentHelpers/appUpdaterHelpers.ts
+++ b/src/utils/componentHelpers/appUpdaterHelpers.ts
@@ -1,4 +1,4 @@
-import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '../platform';
+import { IS_LINUX, IS_MACOS, IS_WINDOWS } from '@/lib/util/platform';
export const SKIP_KEY = 'psysonic_skipped_update_version';
diff --git a/src/utils/componentHelpers/nowPlayingHelpers.ts b/src/utils/componentHelpers/nowPlayingHelpers.ts
index 46e1311d..62a91833 100644
--- a/src/utils/componentHelpers/nowPlayingHelpers.ts
+++ b/src/utils/componentHelpers/nowPlayingHelpers.ts
@@ -1,5 +1,5 @@
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
-import { sanitizeHtml as sanitizeHtmlBase } from '../sanitizeHtml';
+import { sanitizeHtml as sanitizeHtmlBase } from '@/lib/util/sanitizeHtml';
export function formatTime(s: number): string {
if (!s || isNaN(s)) return '0:00';
diff --git a/src/utils/componentHelpers/playlistDetailHelpers.ts b/src/utils/componentHelpers/playlistDetailHelpers.ts
index 76cdb546..6c075a0b 100644
--- a/src/utils/componentHelpers/playlistDetailHelpers.ts
+++ b/src/utils/componentHelpers/playlistDetailHelpers.ts
@@ -1,6 +1,6 @@
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
-import { formatHumanHoursMinutes } from '../format/formatHumanDuration';
-import { formatMb } from '../format/formatBytes';
+import { formatHumanHoursMinutes } from '@/lib/format/formatHumanDuration';
+import { formatMb } from '@/lib/format/formatBytes';
export function sanitizeFilename(name: string): string {
return name
diff --git a/src/utils/componentHelpers/userMgmtHelpers.ts b/src/utils/componentHelpers/userMgmtHelpers.ts
index 6cba9bb8..b73db12f 100644
--- a/src/utils/componentHelpers/userMgmtHelpers.ts
+++ b/src/utils/componentHelpers/userMgmtHelpers.ts
@@ -1,4 +1,4 @@
-import { formatRelativeTime } from '../format/relativeTime';
+import { formatRelativeTime } from '@/lib/format/relativeTime';
/**
* Render a relative time string like "3 hours ago" / "in 2 weeks" with
diff --git a/src/utils/library/albumBrowseCatalogChunk.ts b/src/utils/library/albumBrowseCatalogChunk.ts
index 6105c394..1f2a1b95 100644
--- a/src/utils/library/albumBrowseCatalogChunk.ts
+++ b/src/utils/library/albumBrowseCatalogChunk.ts
@@ -1,5 +1,5 @@
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
-import { dedupeById } from '../dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import { isOfflineBrowseActive } from '@/features/offline';
import { loadOfflineAlbumCatalogChunk } from '@/features/offline';
import type { AlbumBrowseQuery } from './albumBrowseTypes';
diff --git a/src/utils/library/albumBrowseLocal.ts b/src/utils/library/albumBrowseLocal.ts
index 6d77713e..5a3eef53 100644
--- a/src/utils/library/albumBrowseLocal.ts
+++ b/src/utils/library/albumBrowseLocal.ts
@@ -1,7 +1,7 @@
import { libraryAdvancedSearch, libraryListAlbumsByGenre } from '@/lib/api/library';
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
import { libraryScopeForServer } from '@/lib/api/subsonicClient';
-import { dedupeById } from '../dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import { albumToAlbum } from './advancedSearchLocal';
import { sharedServerFilters } from './albumBrowseFilters';
import { albumSortClauses, sortSubsonicAlbums } from './albumBrowseSort';
diff --git a/src/utils/library/albumBrowseNetwork.ts b/src/utils/library/albumBrowseNetwork.ts
index 8870dee5..debab8d2 100644
--- a/src/utils/library/albumBrowseNetwork.ts
+++ b/src/utils/library/albumBrowseNetwork.ts
@@ -1,7 +1,7 @@
import { getAlbumList } from '@/lib/api/subsonicLibrary';
import { getAlbumsByGenre } from '@/lib/api/subsonicGenres';
import type { SubsonicAlbum } from '@/lib/api/subsonicTypes';
-import { dedupeById } from '../dedupeById';
+import { dedupeById } from '@/lib/util/dedupeById';
import {
filterAlbumsByCompilation,
filterAlbumsByYearBounds,
diff --git a/src/utils/mix/luckyMix.ts b/src/utils/mix/luckyMix.ts
index 7c7e801e..49341690 100644
--- a/src/utils/mix/luckyMix.ts
+++ b/src/utils/mix/luckyMix.ts
@@ -4,7 +4,7 @@ import type { SubsonicAlbum, SubsonicSong } from '@/lib/api/subsonicTypes';
import type { QueueItemRef } from '../../store/playerStoreTypes';
import { songToTrack } from '../playback/songToTrack';
import { invoke } from '@tauri-apps/api/core';
-import i18n from '../../i18n';
+import i18n from '@/lib/i18n';
import { useAuthStore } from '../../store/authStore';
import { pushQueueUndoFromGetter } from '../../store/queueUndo';
import { usePlayerStore } from '../../store/playerStore';
diff --git a/src/utils/perf/perfLiveCpuSnapshot.ts b/src/utils/perf/perfLiveCpuSnapshot.ts
index b5ae62b7..5b3dc220 100644
--- a/src/utils/perf/perfLiveCpuSnapshot.ts
+++ b/src/utils/perf/perfLiveCpuSnapshot.ts
@@ -1,4 +1,4 @@
-import { IS_LINUX, IS_MACOS } from '../platform';
+import { IS_LINUX, IS_MACOS } from '@/lib/util/platform';
/** Matches Rust `performance_cpu_snapshot` (Linux `/proc`, macOS sysinfo). */
export function perfLiveCpuSnapshotSupported(): boolean {