Verify persisted settings and save-before-play through production updates
This commit is contained in:
34
frontend/settings-persist.test.js
Normal file
34
frontend/settings-persist.test.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
const source = fs.readFileSync(__dirname + '/app.js', 'utf8');
|
||||||
|
// Exercise the production persistence/profile functions without starting a DOM player.
|
||||||
|
const declaration = name => source.match(new RegExp('(?:async )?function ' + name + '\\([^]*?\\n}'))[0];
|
||||||
|
function fixture(updatedAt) {
|
||||||
|
const settings = { saveBeforePlay: true, autoPreload: true, layout: 'f7-swipe', eq: { preset: 'custom', g: [1,2,3,4,5] } };
|
||||||
|
const context = vm.createContext({ WEB: true, data: { settings, profile: { name: 'test', syncedAt: 200 } }, DEFAULT_SETTINGS: { saveBeforePlay: false, autoPreload: true, layout: 'classic' }, API: { saveData: async () => {} }, profileHeaders: () => ({}), scheduleProfilePush: () => context.pushes++, pushes: 0, fetch: async () => ({ status: 200, json: async () => ({ ok: true, name: 'test', updatedAt, data: { settings: { saveBeforePlay: false } } }) }), localStorage: { getItem: () => JSON.stringify({ settings }) } });
|
||||||
|
vm.runInContext([declaration('loadDataFromStorage'), declaration('applyProfileData'), declaration('pullProfileIfNewer')].join('\n'), context);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
test('stored settings load verbatim instead of reverting to defaults', () => {
|
||||||
|
const c = fixture(100);
|
||||||
|
const loaded = vm.runInContext('loadDataFromStorage()', c);
|
||||||
|
assert.equal(loaded.settings.saveBeforePlay, true);
|
||||||
|
assert.equal(loaded.settings.layout, 'f7-swipe');
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(loaded.settings.eq.g)), [1,2,3,4,5]);
|
||||||
|
});
|
||||||
|
for (const updatedAt of [100,200]) test(`profile timestamp ${updatedAt} cannot overwrite local settings synced at 200`, async () => {
|
||||||
|
const c = fixture(updatedAt), previous = c.data.settings;
|
||||||
|
await vm.runInContext('pullProfileIfNewer()', c);
|
||||||
|
assert.equal(c.data.settings, previous);
|
||||||
|
assert.equal(c.pushes, 1);
|
||||||
|
});
|
||||||
|
test('newer profile intentionally replaces settings and restores missing defaults', async () => {
|
||||||
|
const c = fixture(300);
|
||||||
|
await vm.runInContext('pullProfileIfNewer()', c);
|
||||||
|
assert.equal(c.data.settings.saveBeforePlay, false);
|
||||||
|
assert.equal(c.data.settings.autoPreload, true);
|
||||||
|
assert.equal(c.data.settings.layout, 'classic');
|
||||||
|
assert.equal(c.data.profile.syncedAt, 300);
|
||||||
|
});
|
||||||
@@ -335,3 +335,24 @@ No product behavior changes are involved in these measurement corrections.
|
|||||||
Keep browser/build/test processes idle during full timing measurements. WebKit's
|
Keep browser/build/test processes idle during full timing measurements. WebKit's
|
||||||
zero long-task field means unavailable, not zero work. Autoplay-blocked/media-ready
|
zero long-task field means unavailable, not zero work. Autoplay-blocked/media-ready
|
||||||
null results do not establish playback performance or iPhone audio continuity.
|
null results do not establish playback performance or iPhone audio continuity.
|
||||||
|
|
||||||
|
## Existing-install settings preservation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node perf/settings-persist.mjs --browser all --target main --allow-unsupported-webkit
|
||||||
|
node perf/settings-persist.mjs --browser all --target 0b8e398 --allow-unsupported-webkit --out perf/results/settings-persist-live-base.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The harness archives b77938a and the requested target into worktree-local scratch,
|
||||||
|
serves actual Bun servers at the same origin, sets values through Settings UI
|
||||||
|
handlers, reloads, invokes the old banner's actual Refresh UI button, and diffs
|
||||||
|
every `_ytpdata.settings` key and control. A real local H.264/AAC clip verifies
|
||||||
|
OPFS storage and blob playback before/after in Chromium. A local HTTP proxy
|
||||||
|
mocks profile timestamps for worker-owned fetches on both engines and tests
|
||||||
|
backup import. There are no external profile writes or media requests.
|
||||||
|
|
||||||
|
Failures exit 1. Linux WebKit has no native OPFS; without the explicit allowance
|
||||||
|
the unverified storage capability exits 2. The allowance only skips unsupported
|
||||||
|
storage assertions; every preservation/profile/import assertion still runs.
|
||||||
|
No storage shim substitutes for a genuine iPhone test. Snapshot results and the
|
||||||
|
per-key table are in `plans/settings-persist-investigation.md`.
|
||||||
|
|||||||
708
perf/results/settings-persist-live-base.json
Normal file
708
perf/results/settings-persist-live-base.json
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
{
|
||||||
|
"revision": "0b8e39846990eb178a28746d3d111bb0790d8275",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"engine": "chromium",
|
||||||
|
"errors": [
|
||||||
|
"Cannot read properties of null (reading 'addEventListener')",
|
||||||
|
"Cannot read properties of null (reading 'addEventListener')"
|
||||||
|
],
|
||||||
|
"profileChecks": [
|
||||||
|
{
|
||||||
|
"stage": "older timestamp ignored",
|
||||||
|
"serverUpdatedAt": 100,
|
||||||
|
"localSyncedAt": 200,
|
||||||
|
"saveBeforePlay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage": "older settings with newer timestamp adopted",
|
||||||
|
"serverUpdatedAt": 300,
|
||||||
|
"localSyncedAt": 200,
|
||||||
|
"saveBeforePlay": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"behaviour": [
|
||||||
|
{
|
||||||
|
"stage": "old",
|
||||||
|
"supported": true,
|
||||||
|
"bytes": 30290,
|
||||||
|
"cached": true,
|
||||||
|
"localPlayback": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage": "upgraded",
|
||||||
|
"supported": true,
|
||||||
|
"bytes": 30290,
|
||||||
|
"cached": true,
|
||||||
|
"localPlayback": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"diff": [
|
||||||
|
{
|
||||||
|
"key": "quality",
|
||||||
|
"before": "360p",
|
||||||
|
"after": "360p",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "volume",
|
||||||
|
"before": 1,
|
||||||
|
"after": 1,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "audioOnly",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoPreload",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "p2pShare",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "p2pReceive",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "saveBeforePlay",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "repeatMode",
|
||||||
|
"before": "all",
|
||||||
|
"after": "all",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "loopOne",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "shuffle",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "theme",
|
||||||
|
"before": "light",
|
||||||
|
"after": "light",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontScale",
|
||||||
|
"before": "large",
|
||||||
|
"after": "large",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "levelVolume",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "liveAudioIOS",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "parallelSaves",
|
||||||
|
"before": 2,
|
||||||
|
"after": 2,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "density",
|
||||||
|
"before": "compact",
|
||||||
|
"after": "compact",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "perfMode",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "reduceMotion",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "layout",
|
||||||
|
"before": "f7-swipe",
|
||||||
|
"after": "f7-swipe",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoBackupEnabled",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoBackupIntervalDays",
|
||||||
|
"before": 7,
|
||||||
|
"after": 7,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "serviceVideo",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "foldedCards",
|
||||||
|
"before": {},
|
||||||
|
"after": {},
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "eq",
|
||||||
|
"before": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"old": {
|
||||||
|
"quality": "360p",
|
||||||
|
"volume": 1,
|
||||||
|
"audioOnly": false,
|
||||||
|
"autoPreload": true,
|
||||||
|
"p2pShare": true,
|
||||||
|
"p2pReceive": true,
|
||||||
|
"saveBeforePlay": true,
|
||||||
|
"repeatMode": "all",
|
||||||
|
"loopOne": true,
|
||||||
|
"shuffle": false,
|
||||||
|
"theme": "light",
|
||||||
|
"fontScale": "large",
|
||||||
|
"levelVolume": false,
|
||||||
|
"liveAudioIOS": false,
|
||||||
|
"parallelSaves": 2,
|
||||||
|
"density": "compact",
|
||||||
|
"perfMode": true,
|
||||||
|
"reduceMotion": true,
|
||||||
|
"layout": "f7-swipe",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupIntervalDays": 7,
|
||||||
|
"serviceVideo": false,
|
||||||
|
"foldedCards": {},
|
||||||
|
"eq": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oldUI": {
|
||||||
|
"setSaveBeforePlay": true,
|
||||||
|
"setAutoPreload": true,
|
||||||
|
"setAudioOnly": false,
|
||||||
|
"setQuality": "360p",
|
||||||
|
"setRepeat": true,
|
||||||
|
"setLoopOne": true,
|
||||||
|
"setTheme": "light",
|
||||||
|
"setFont": "large",
|
||||||
|
"setDensity": "compact",
|
||||||
|
"setPerf": true,
|
||||||
|
"setMotion": true,
|
||||||
|
"setParallelSaves": "2",
|
||||||
|
"setLevelVolume": false,
|
||||||
|
"setLayout": "f7-swipe"
|
||||||
|
},
|
||||||
|
"oldRaw": "{\"playlists\":[],\"history\":[],\"queue\":[],\"customVideos\":[],\"pinnedPlaylists\":[],\"settings\":{\"quality\":\"360p\",\"volume\":1,\"audioOnly\":false,\"autoPreload\":true,\"p2pShare\":true,\"p2pReceive\":true,\"saveBeforePlay\":true,\"repeatMode\":\"all\",\"loopOne\":true,\"shuffle\":false,\"theme\":\"light\",\"fontScale\":\"large\",\"levelVolume\":false,\"liveAudioIOS\":false,\"parallelSaves\":2,\"density\":\"compact\",\"perfMode\":true,\"reduceMotion\":true,\"layout\":\"f7-swipe\",\"autoBackupEnabled\":false,\"autoBackupIntervalDays\":7,\"serviceVideo\":false,\"foldedCards\":{},\"eq\":{\"preset\":\"bass\",\"g\":[5,7,5,3,1,0,0,0,0,0]}},\"resumePositions\":{},\"rememberPos\":{},\"playCount\":{},\"abMarkers\":{},\"bookmarks\":{},\"lyricOffsets\":{},\"stats\":null,\"lastAutoBackup\":0,\"profile\":null}",
|
||||||
|
"newTag": "00d7fed5abcb",
|
||||||
|
"upgraded": {
|
||||||
|
"quality": "360p",
|
||||||
|
"volume": 1,
|
||||||
|
"audioOnly": false,
|
||||||
|
"autoPreload": true,
|
||||||
|
"p2pShare": true,
|
||||||
|
"p2pReceive": true,
|
||||||
|
"saveBeforePlay": true,
|
||||||
|
"repeatMode": "all",
|
||||||
|
"loopOne": true,
|
||||||
|
"shuffle": false,
|
||||||
|
"theme": "light",
|
||||||
|
"fontScale": "large",
|
||||||
|
"levelVolume": false,
|
||||||
|
"liveAudioIOS": false,
|
||||||
|
"parallelSaves": 2,
|
||||||
|
"density": "compact",
|
||||||
|
"perfMode": true,
|
||||||
|
"reduceMotion": true,
|
||||||
|
"layout": "f7-swipe",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupIntervalDays": 7,
|
||||||
|
"serviceVideo": false,
|
||||||
|
"foldedCards": {},
|
||||||
|
"eq": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newRaw": "{\"playlists\":[],\"history\":[{\"id\":\"benchmedia01\",\"title\":\"Persisted settings local clip\",\"channel\":\"Settings probe\",\"channelId\":\"\",\"channelUrl\":\"\",\"duration\":3,\"thumbnail\":\"\"}],\"queue\":[],\"customVideos\":[],\"pinnedPlaylists\":[],\"resumePositions\":{},\"rememberPos\":{},\"playCount\":{\"benchmedia01\":1},\"abMarkers\":{},\"bookmarks\":{},\"lyricOffsets\":{},\"stats\":null,\"lastAutoBackup\":0,\"settings\":{\"quality\":\"360p\",\"volume\":1,\"audioOnly\":false,\"autoPreload\":true,\"p2pShare\":true,\"p2pReceive\":true,\"saveBeforePlay\":true,\"repeatMode\":\"all\",\"loopOne\":true,\"shuffle\":false,\"theme\":\"light\",\"fontScale\":\"large\",\"levelVolume\":false,\"liveAudioIOS\":false,\"parallelSaves\":2,\"density\":\"compact\",\"perfMode\":true,\"reduceMotion\":true,\"layout\":\"f7-swipe\",\"autoBackupEnabled\":false,\"autoBackupIntervalDays\":7,\"serviceVideo\":false,\"foldedCards\":{},\"eq\":{\"preset\":\"bass\",\"g\":[5,7,5,3,1,0,0,0,0,0]}},\"profile\":null}",
|
||||||
|
"newUI": {
|
||||||
|
"setSaveBeforePlay": true,
|
||||||
|
"setAutoPreload": true,
|
||||||
|
"setAudioOnly": false,
|
||||||
|
"setQuality": "360p",
|
||||||
|
"setRepeat": true,
|
||||||
|
"setLoopOne": true,
|
||||||
|
"setTheme": "light",
|
||||||
|
"setFont": "large",
|
||||||
|
"setDensity": "compact",
|
||||||
|
"setPerf": true,
|
||||||
|
"setMotion": true,
|
||||||
|
"setParallelSaves": "2",
|
||||||
|
"setLevelVolume": false,
|
||||||
|
"setLayout": "f7-swipe"
|
||||||
|
},
|
||||||
|
"importPreserved": true,
|
||||||
|
"userDataGets": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"engine": "webkit",
|
||||||
|
"errors": [],
|
||||||
|
"profileChecks": [
|
||||||
|
{
|
||||||
|
"stage": "older timestamp ignored",
|
||||||
|
"serverUpdatedAt": 100,
|
||||||
|
"localSyncedAt": 200,
|
||||||
|
"saveBeforePlay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage": "older settings with newer timestamp adopted",
|
||||||
|
"serverUpdatedAt": 300,
|
||||||
|
"localSyncedAt": 200,
|
||||||
|
"saveBeforePlay": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"behaviour": [
|
||||||
|
{
|
||||||
|
"stage": "old",
|
||||||
|
"supported": false,
|
||||||
|
"note": "Native OPFS unavailable; no shim"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage": "upgraded",
|
||||||
|
"supported": false,
|
||||||
|
"note": "Native OPFS unavailable; no shim"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"diff": [
|
||||||
|
{
|
||||||
|
"key": "quality",
|
||||||
|
"before": "360p",
|
||||||
|
"after": "360p",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "volume",
|
||||||
|
"before": 1,
|
||||||
|
"after": 1,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "audioOnly",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoPreload",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "p2pShare",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "p2pReceive",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "saveBeforePlay",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "repeatMode",
|
||||||
|
"before": "all",
|
||||||
|
"after": "all",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "loopOne",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "shuffle",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "theme",
|
||||||
|
"before": "light",
|
||||||
|
"after": "light",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontScale",
|
||||||
|
"before": "large",
|
||||||
|
"after": "large",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "levelVolume",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "liveAudioIOS",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "parallelSaves",
|
||||||
|
"before": 2,
|
||||||
|
"after": 2,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "density",
|
||||||
|
"before": "compact",
|
||||||
|
"after": "compact",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "perfMode",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "reduceMotion",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "layout",
|
||||||
|
"before": "f7-swipe",
|
||||||
|
"after": "f7-swipe",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoBackupEnabled",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoBackupIntervalDays",
|
||||||
|
"before": 7,
|
||||||
|
"after": 7,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "serviceVideo",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "foldedCards",
|
||||||
|
"before": {},
|
||||||
|
"after": {},
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "eq",
|
||||||
|
"before": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"old": {
|
||||||
|
"quality": "360p",
|
||||||
|
"volume": 1,
|
||||||
|
"audioOnly": false,
|
||||||
|
"autoPreload": true,
|
||||||
|
"p2pShare": true,
|
||||||
|
"p2pReceive": true,
|
||||||
|
"saveBeforePlay": true,
|
||||||
|
"repeatMode": "all",
|
||||||
|
"loopOne": true,
|
||||||
|
"shuffle": false,
|
||||||
|
"theme": "light",
|
||||||
|
"fontScale": "large",
|
||||||
|
"levelVolume": false,
|
||||||
|
"liveAudioIOS": false,
|
||||||
|
"parallelSaves": 2,
|
||||||
|
"density": "compact",
|
||||||
|
"perfMode": true,
|
||||||
|
"reduceMotion": true,
|
||||||
|
"layout": "f7-swipe",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupIntervalDays": 7,
|
||||||
|
"serviceVideo": false,
|
||||||
|
"foldedCards": {},
|
||||||
|
"eq": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oldUI": {
|
||||||
|
"setSaveBeforePlay": true,
|
||||||
|
"setAutoPreload": true,
|
||||||
|
"setAudioOnly": false,
|
||||||
|
"setQuality": "360p",
|
||||||
|
"setRepeat": true,
|
||||||
|
"setLoopOne": true,
|
||||||
|
"setTheme": "light",
|
||||||
|
"setFont": "large",
|
||||||
|
"setDensity": "compact",
|
||||||
|
"setPerf": true,
|
||||||
|
"setMotion": true,
|
||||||
|
"setParallelSaves": "2",
|
||||||
|
"setLevelVolume": false,
|
||||||
|
"setLayout": "f7-swipe"
|
||||||
|
},
|
||||||
|
"oldRaw": "{\"playlists\":[],\"history\":[],\"queue\":[],\"customVideos\":[],\"pinnedPlaylists\":[],\"settings\":{\"quality\":\"360p\",\"volume\":1,\"audioOnly\":false,\"autoPreload\":true,\"p2pShare\":true,\"p2pReceive\":true,\"saveBeforePlay\":true,\"repeatMode\":\"all\",\"loopOne\":true,\"shuffle\":false,\"theme\":\"light\",\"fontScale\":\"large\",\"levelVolume\":false,\"liveAudioIOS\":false,\"parallelSaves\":2,\"density\":\"compact\",\"perfMode\":true,\"reduceMotion\":true,\"layout\":\"f7-swipe\",\"autoBackupEnabled\":false,\"autoBackupIntervalDays\":7,\"serviceVideo\":false,\"foldedCards\":{},\"eq\":{\"preset\":\"bass\",\"g\":[5,7,5,3,1,0,0,0,0,0]}},\"resumePositions\":{},\"rememberPos\":{},\"playCount\":{},\"abMarkers\":{},\"bookmarks\":{},\"lyricOffsets\":{},\"stats\":null,\"lastAutoBackup\":0,\"profile\":null}",
|
||||||
|
"newTag": "00d7fed5abcb",
|
||||||
|
"upgraded": {
|
||||||
|
"quality": "360p",
|
||||||
|
"volume": 1,
|
||||||
|
"audioOnly": false,
|
||||||
|
"autoPreload": true,
|
||||||
|
"p2pShare": true,
|
||||||
|
"p2pReceive": true,
|
||||||
|
"saveBeforePlay": true,
|
||||||
|
"repeatMode": "all",
|
||||||
|
"loopOne": true,
|
||||||
|
"shuffle": false,
|
||||||
|
"theme": "light",
|
||||||
|
"fontScale": "large",
|
||||||
|
"levelVolume": false,
|
||||||
|
"liveAudioIOS": false,
|
||||||
|
"parallelSaves": 2,
|
||||||
|
"density": "compact",
|
||||||
|
"perfMode": true,
|
||||||
|
"reduceMotion": true,
|
||||||
|
"layout": "f7-swipe",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupIntervalDays": 7,
|
||||||
|
"serviceVideo": false,
|
||||||
|
"foldedCards": {},
|
||||||
|
"eq": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newRaw": "{\"playlists\":[],\"history\":[],\"queue\":[],\"customVideos\":[],\"pinnedPlaylists\":[],\"settings\":{\"quality\":\"360p\",\"volume\":1,\"audioOnly\":false,\"autoPreload\":true,\"p2pShare\":true,\"p2pReceive\":true,\"saveBeforePlay\":true,\"repeatMode\":\"all\",\"loopOne\":true,\"shuffle\":false,\"theme\":\"light\",\"fontScale\":\"large\",\"levelVolume\":false,\"liveAudioIOS\":false,\"parallelSaves\":2,\"density\":\"compact\",\"perfMode\":true,\"reduceMotion\":true,\"layout\":\"f7-swipe\",\"autoBackupEnabled\":false,\"autoBackupIntervalDays\":7,\"serviceVideo\":false,\"foldedCards\":{},\"eq\":{\"preset\":\"bass\",\"g\":[5,7,5,3,1,0,0,0,0,0]}},\"resumePositions\":{},\"rememberPos\":{},\"playCount\":{},\"abMarkers\":{},\"bookmarks\":{},\"lyricOffsets\":{},\"stats\":null,\"lastAutoBackup\":0,\"profile\":null}",
|
||||||
|
"newUI": {
|
||||||
|
"setSaveBeforePlay": true,
|
||||||
|
"setAutoPreload": true,
|
||||||
|
"setAudioOnly": false,
|
||||||
|
"setQuality": "360p",
|
||||||
|
"setRepeat": true,
|
||||||
|
"setLoopOne": true,
|
||||||
|
"setTheme": "light",
|
||||||
|
"setFont": "large",
|
||||||
|
"setDensity": "compact",
|
||||||
|
"setPerf": true,
|
||||||
|
"setMotion": true,
|
||||||
|
"setParallelSaves": "2",
|
||||||
|
"setLevelVolume": false,
|
||||||
|
"setLayout": "f7-swipe"
|
||||||
|
},
|
||||||
|
"importPreserved": true,
|
||||||
|
"userDataGets": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"failures": [],
|
||||||
|
"unsupported": [
|
||||||
|
"webkit",
|
||||||
|
"webkit"
|
||||||
|
]
|
||||||
|
}
|
||||||
708
perf/results/settings-persist.json
Normal file
708
perf/results/settings-persist.json
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
{
|
||||||
|
"revision": "cf27d4f15a29747123437862ab4e461fd78178e6",
|
||||||
|
"oldTag": "1175f1a1d2c1",
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"engine": "chromium",
|
||||||
|
"errors": [
|
||||||
|
"Cannot read properties of null (reading 'addEventListener')",
|
||||||
|
"Cannot read properties of null (reading 'addEventListener')"
|
||||||
|
],
|
||||||
|
"profileChecks": [
|
||||||
|
{
|
||||||
|
"stage": "older timestamp ignored",
|
||||||
|
"serverUpdatedAt": 100,
|
||||||
|
"localSyncedAt": 200,
|
||||||
|
"saveBeforePlay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage": "older settings with newer timestamp adopted",
|
||||||
|
"serverUpdatedAt": 300,
|
||||||
|
"localSyncedAt": 200,
|
||||||
|
"saveBeforePlay": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"behaviour": [
|
||||||
|
{
|
||||||
|
"stage": "old",
|
||||||
|
"supported": true,
|
||||||
|
"bytes": 30290,
|
||||||
|
"cached": true,
|
||||||
|
"localPlayback": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage": "upgraded",
|
||||||
|
"supported": true,
|
||||||
|
"bytes": 30290,
|
||||||
|
"cached": true,
|
||||||
|
"localPlayback": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"diff": [
|
||||||
|
{
|
||||||
|
"key": "quality",
|
||||||
|
"before": "360p",
|
||||||
|
"after": "360p",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "volume",
|
||||||
|
"before": 1,
|
||||||
|
"after": 1,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "audioOnly",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoPreload",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "p2pShare",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "p2pReceive",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "saveBeforePlay",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "repeatMode",
|
||||||
|
"before": "all",
|
||||||
|
"after": "all",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "loopOne",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "shuffle",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "theme",
|
||||||
|
"before": "light",
|
||||||
|
"after": "light",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontScale",
|
||||||
|
"before": "large",
|
||||||
|
"after": "large",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "levelVolume",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "liveAudioIOS",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "parallelSaves",
|
||||||
|
"before": 2,
|
||||||
|
"after": 2,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "density",
|
||||||
|
"before": "compact",
|
||||||
|
"after": "compact",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "perfMode",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "reduceMotion",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "layout",
|
||||||
|
"before": "f7-swipe",
|
||||||
|
"after": "f7-swipe",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoBackupEnabled",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoBackupIntervalDays",
|
||||||
|
"before": 7,
|
||||||
|
"after": 7,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "serviceVideo",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "foldedCards",
|
||||||
|
"before": {},
|
||||||
|
"after": {},
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "eq",
|
||||||
|
"before": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"old": {
|
||||||
|
"quality": "360p",
|
||||||
|
"volume": 1,
|
||||||
|
"audioOnly": false,
|
||||||
|
"autoPreload": true,
|
||||||
|
"p2pShare": true,
|
||||||
|
"p2pReceive": true,
|
||||||
|
"saveBeforePlay": true,
|
||||||
|
"repeatMode": "all",
|
||||||
|
"loopOne": true,
|
||||||
|
"shuffle": false,
|
||||||
|
"theme": "light",
|
||||||
|
"fontScale": "large",
|
||||||
|
"levelVolume": false,
|
||||||
|
"liveAudioIOS": false,
|
||||||
|
"parallelSaves": 2,
|
||||||
|
"density": "compact",
|
||||||
|
"perfMode": true,
|
||||||
|
"reduceMotion": true,
|
||||||
|
"layout": "f7-swipe",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupIntervalDays": 7,
|
||||||
|
"serviceVideo": false,
|
||||||
|
"foldedCards": {},
|
||||||
|
"eq": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oldUI": {
|
||||||
|
"setSaveBeforePlay": true,
|
||||||
|
"setAutoPreload": true,
|
||||||
|
"setAudioOnly": false,
|
||||||
|
"setQuality": "360p",
|
||||||
|
"setRepeat": true,
|
||||||
|
"setLoopOne": true,
|
||||||
|
"setTheme": "light",
|
||||||
|
"setFont": "large",
|
||||||
|
"setDensity": "compact",
|
||||||
|
"setPerf": true,
|
||||||
|
"setMotion": true,
|
||||||
|
"setParallelSaves": "2",
|
||||||
|
"setLevelVolume": false,
|
||||||
|
"setLayout": "f7-swipe"
|
||||||
|
},
|
||||||
|
"oldRaw": "{\"playlists\":[],\"history\":[],\"queue\":[],\"customVideos\":[],\"pinnedPlaylists\":[],\"settings\":{\"quality\":\"360p\",\"volume\":1,\"audioOnly\":false,\"autoPreload\":true,\"p2pShare\":true,\"p2pReceive\":true,\"saveBeforePlay\":true,\"repeatMode\":\"all\",\"loopOne\":true,\"shuffle\":false,\"theme\":\"light\",\"fontScale\":\"large\",\"levelVolume\":false,\"liveAudioIOS\":false,\"parallelSaves\":2,\"density\":\"compact\",\"perfMode\":true,\"reduceMotion\":true,\"layout\":\"f7-swipe\",\"autoBackupEnabled\":false,\"autoBackupIntervalDays\":7,\"serviceVideo\":false,\"foldedCards\":{},\"eq\":{\"preset\":\"bass\",\"g\":[5,7,5,3,1,0,0,0,0,0]}},\"resumePositions\":{},\"rememberPos\":{},\"playCount\":{},\"abMarkers\":{},\"bookmarks\":{},\"lyricOffsets\":{},\"stats\":null,\"lastAutoBackup\":0,\"profile\":null}",
|
||||||
|
"newTag": "ad9d32e3feb3",
|
||||||
|
"upgraded": {
|
||||||
|
"quality": "360p",
|
||||||
|
"volume": 1,
|
||||||
|
"audioOnly": false,
|
||||||
|
"autoPreload": true,
|
||||||
|
"p2pShare": true,
|
||||||
|
"p2pReceive": true,
|
||||||
|
"saveBeforePlay": true,
|
||||||
|
"repeatMode": "all",
|
||||||
|
"loopOne": true,
|
||||||
|
"shuffle": false,
|
||||||
|
"theme": "light",
|
||||||
|
"fontScale": "large",
|
||||||
|
"levelVolume": false,
|
||||||
|
"liveAudioIOS": false,
|
||||||
|
"parallelSaves": 2,
|
||||||
|
"density": "compact",
|
||||||
|
"perfMode": true,
|
||||||
|
"reduceMotion": true,
|
||||||
|
"layout": "f7-swipe",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupIntervalDays": 7,
|
||||||
|
"serviceVideo": false,
|
||||||
|
"foldedCards": {},
|
||||||
|
"eq": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newRaw": "{\"playlists\":[],\"history\":[{\"id\":\"benchmedia01\",\"title\":\"Persisted settings local clip\",\"channel\":\"Settings probe\",\"channelId\":\"\",\"channelUrl\":\"\",\"duration\":3,\"thumbnail\":\"\"}],\"queue\":[],\"customVideos\":[],\"pinnedPlaylists\":[],\"resumePositions\":{},\"rememberPos\":{},\"playCount\":{\"benchmedia01\":1},\"abMarkers\":{},\"bookmarks\":{},\"lyricOffsets\":{},\"stats\":null,\"lastAutoBackup\":0,\"settings\":{\"quality\":\"360p\",\"volume\":1,\"audioOnly\":false,\"autoPreload\":true,\"p2pShare\":true,\"p2pReceive\":true,\"saveBeforePlay\":true,\"repeatMode\":\"all\",\"loopOne\":true,\"shuffle\":false,\"theme\":\"light\",\"fontScale\":\"large\",\"levelVolume\":false,\"liveAudioIOS\":false,\"parallelSaves\":2,\"density\":\"compact\",\"perfMode\":true,\"reduceMotion\":true,\"layout\":\"f7-swipe\",\"autoBackupEnabled\":false,\"autoBackupIntervalDays\":7,\"serviceVideo\":false,\"foldedCards\":{},\"eq\":{\"preset\":\"bass\",\"g\":[5,7,5,3,1,0,0,0,0,0]}},\"profile\":null}",
|
||||||
|
"newUI": {
|
||||||
|
"setSaveBeforePlay": true,
|
||||||
|
"setAutoPreload": true,
|
||||||
|
"setAudioOnly": false,
|
||||||
|
"setQuality": "360p",
|
||||||
|
"setRepeat": true,
|
||||||
|
"setLoopOne": true,
|
||||||
|
"setTheme": "light",
|
||||||
|
"setFont": "large",
|
||||||
|
"setDensity": "compact",
|
||||||
|
"setPerf": true,
|
||||||
|
"setMotion": true,
|
||||||
|
"setParallelSaves": "2",
|
||||||
|
"setLevelVolume": false,
|
||||||
|
"setLayout": "f7-swipe"
|
||||||
|
},
|
||||||
|
"importPreserved": true,
|
||||||
|
"userDataGets": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"engine": "webkit",
|
||||||
|
"errors": [],
|
||||||
|
"profileChecks": [
|
||||||
|
{
|
||||||
|
"stage": "older timestamp ignored",
|
||||||
|
"serverUpdatedAt": 100,
|
||||||
|
"localSyncedAt": 200,
|
||||||
|
"saveBeforePlay": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage": "older settings with newer timestamp adopted",
|
||||||
|
"serverUpdatedAt": 300,
|
||||||
|
"localSyncedAt": 200,
|
||||||
|
"saveBeforePlay": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"behaviour": [
|
||||||
|
{
|
||||||
|
"stage": "old",
|
||||||
|
"supported": false,
|
||||||
|
"note": "Native OPFS unavailable; no shim"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"stage": "upgraded",
|
||||||
|
"supported": false,
|
||||||
|
"note": "Native OPFS unavailable; no shim"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"diff": [
|
||||||
|
{
|
||||||
|
"key": "quality",
|
||||||
|
"before": "360p",
|
||||||
|
"after": "360p",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "volume",
|
||||||
|
"before": 1,
|
||||||
|
"after": 1,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "audioOnly",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoPreload",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "p2pShare",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "p2pReceive",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "saveBeforePlay",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "repeatMode",
|
||||||
|
"before": "all",
|
||||||
|
"after": "all",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "loopOne",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "shuffle",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "theme",
|
||||||
|
"before": "light",
|
||||||
|
"after": "light",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "fontScale",
|
||||||
|
"before": "large",
|
||||||
|
"after": "large",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "levelVolume",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "liveAudioIOS",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "parallelSaves",
|
||||||
|
"before": 2,
|
||||||
|
"after": 2,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "density",
|
||||||
|
"before": "compact",
|
||||||
|
"after": "compact",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "perfMode",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "reduceMotion",
|
||||||
|
"before": true,
|
||||||
|
"after": true,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "layout",
|
||||||
|
"before": "f7-swipe",
|
||||||
|
"after": "f7-swipe",
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoBackupEnabled",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "autoBackupIntervalDays",
|
||||||
|
"before": 7,
|
||||||
|
"after": 7,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "serviceVideo",
|
||||||
|
"before": false,
|
||||||
|
"after": false,
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "foldedCards",
|
||||||
|
"before": {},
|
||||||
|
"after": {},
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "eq",
|
||||||
|
"before": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"equal": true,
|
||||||
|
"expectedMigration": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"old": {
|
||||||
|
"quality": "360p",
|
||||||
|
"volume": 1,
|
||||||
|
"audioOnly": false,
|
||||||
|
"autoPreload": true,
|
||||||
|
"p2pShare": true,
|
||||||
|
"p2pReceive": true,
|
||||||
|
"saveBeforePlay": true,
|
||||||
|
"repeatMode": "all",
|
||||||
|
"loopOne": true,
|
||||||
|
"shuffle": false,
|
||||||
|
"theme": "light",
|
||||||
|
"fontScale": "large",
|
||||||
|
"levelVolume": false,
|
||||||
|
"liveAudioIOS": false,
|
||||||
|
"parallelSaves": 2,
|
||||||
|
"density": "compact",
|
||||||
|
"perfMode": true,
|
||||||
|
"reduceMotion": true,
|
||||||
|
"layout": "f7-swipe",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupIntervalDays": 7,
|
||||||
|
"serviceVideo": false,
|
||||||
|
"foldedCards": {},
|
||||||
|
"eq": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oldUI": {
|
||||||
|
"setSaveBeforePlay": true,
|
||||||
|
"setAutoPreload": true,
|
||||||
|
"setAudioOnly": false,
|
||||||
|
"setQuality": "360p",
|
||||||
|
"setRepeat": true,
|
||||||
|
"setLoopOne": true,
|
||||||
|
"setTheme": "light",
|
||||||
|
"setFont": "large",
|
||||||
|
"setDensity": "compact",
|
||||||
|
"setPerf": true,
|
||||||
|
"setMotion": true,
|
||||||
|
"setParallelSaves": "2",
|
||||||
|
"setLevelVolume": false,
|
||||||
|
"setLayout": "f7-swipe"
|
||||||
|
},
|
||||||
|
"oldRaw": "{\"playlists\":[],\"history\":[],\"queue\":[],\"customVideos\":[],\"pinnedPlaylists\":[],\"settings\":{\"quality\":\"360p\",\"volume\":1,\"audioOnly\":false,\"autoPreload\":true,\"p2pShare\":true,\"p2pReceive\":true,\"saveBeforePlay\":true,\"repeatMode\":\"all\",\"loopOne\":true,\"shuffle\":false,\"theme\":\"light\",\"fontScale\":\"large\",\"levelVolume\":false,\"liveAudioIOS\":false,\"parallelSaves\":2,\"density\":\"compact\",\"perfMode\":true,\"reduceMotion\":true,\"layout\":\"f7-swipe\",\"autoBackupEnabled\":false,\"autoBackupIntervalDays\":7,\"serviceVideo\":false,\"foldedCards\":{},\"eq\":{\"preset\":\"bass\",\"g\":[5,7,5,3,1,0,0,0,0,0]}},\"resumePositions\":{},\"rememberPos\":{},\"playCount\":{},\"abMarkers\":{},\"bookmarks\":{},\"lyricOffsets\":{},\"stats\":null,\"lastAutoBackup\":0,\"profile\":null}",
|
||||||
|
"newTag": "ad9d32e3feb3",
|
||||||
|
"upgraded": {
|
||||||
|
"quality": "360p",
|
||||||
|
"volume": 1,
|
||||||
|
"audioOnly": false,
|
||||||
|
"autoPreload": true,
|
||||||
|
"p2pShare": true,
|
||||||
|
"p2pReceive": true,
|
||||||
|
"saveBeforePlay": true,
|
||||||
|
"repeatMode": "all",
|
||||||
|
"loopOne": true,
|
||||||
|
"shuffle": false,
|
||||||
|
"theme": "light",
|
||||||
|
"fontScale": "large",
|
||||||
|
"levelVolume": false,
|
||||||
|
"liveAudioIOS": false,
|
||||||
|
"parallelSaves": 2,
|
||||||
|
"density": "compact",
|
||||||
|
"perfMode": true,
|
||||||
|
"reduceMotion": true,
|
||||||
|
"layout": "f7-swipe",
|
||||||
|
"autoBackupEnabled": false,
|
||||||
|
"autoBackupIntervalDays": 7,
|
||||||
|
"serviceVideo": false,
|
||||||
|
"foldedCards": {},
|
||||||
|
"eq": {
|
||||||
|
"preset": "bass",
|
||||||
|
"g": [
|
||||||
|
5,
|
||||||
|
7,
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"newRaw": "{\"playlists\":[],\"history\":[],\"queue\":[],\"customVideos\":[],\"pinnedPlaylists\":[],\"settings\":{\"quality\":\"360p\",\"volume\":1,\"audioOnly\":false,\"autoPreload\":true,\"p2pShare\":true,\"p2pReceive\":true,\"saveBeforePlay\":true,\"repeatMode\":\"all\",\"loopOne\":true,\"shuffle\":false,\"theme\":\"light\",\"fontScale\":\"large\",\"levelVolume\":false,\"liveAudioIOS\":false,\"parallelSaves\":2,\"density\":\"compact\",\"perfMode\":true,\"reduceMotion\":true,\"layout\":\"f7-swipe\",\"autoBackupEnabled\":false,\"autoBackupIntervalDays\":7,\"serviceVideo\":false,\"foldedCards\":{},\"eq\":{\"preset\":\"bass\",\"g\":[5,7,5,3,1,0,0,0,0,0]}},\"resumePositions\":{},\"rememberPos\":{},\"playCount\":{},\"abMarkers\":{},\"bookmarks\":{},\"lyricOffsets\":{},\"stats\":null,\"lastAutoBackup\":0,\"profile\":null}",
|
||||||
|
"newUI": {
|
||||||
|
"setSaveBeforePlay": true,
|
||||||
|
"setAutoPreload": true,
|
||||||
|
"setAudioOnly": false,
|
||||||
|
"setQuality": "360p",
|
||||||
|
"setRepeat": true,
|
||||||
|
"setLoopOne": true,
|
||||||
|
"setTheme": "light",
|
||||||
|
"setFont": "large",
|
||||||
|
"setDensity": "compact",
|
||||||
|
"setPerf": true,
|
||||||
|
"setMotion": true,
|
||||||
|
"setParallelSaves": "2",
|
||||||
|
"setLevelVolume": false,
|
||||||
|
"setLayout": "f7-swipe"
|
||||||
|
},
|
||||||
|
"importPreserved": true,
|
||||||
|
"userDataGets": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"failures": [],
|
||||||
|
"unsupported": [
|
||||||
|
"webkit",
|
||||||
|
"webkit"
|
||||||
|
]
|
||||||
|
}
|
||||||
82
perf/settings-persist.mjs
Normal file
82
perf/settings-persist.mjs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Existing-install migration and profile/import settings gate; all servers and media stay local.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {spawn,execFileSync} from 'node:child_process';
|
||||||
|
import {mkdtempSync,mkdirSync,writeFileSync,readFileSync,symlinkSync,unlinkSync,rmSync,statSync} from 'node:fs';
|
||||||
|
import {join,resolve} from 'node:path';
|
||||||
|
import {createServer} from 'node:net';
|
||||||
|
import {createServer as createHttpServer, request as httpRequest} from 'node:http';
|
||||||
|
import {createRequire} from 'node:module';
|
||||||
|
import {chromium,webkit} from 'playwright';
|
||||||
|
const repo=resolve(import.meta.dirname,'..'),args=process.argv.slice(2);
|
||||||
|
const opt=(k,d)=>args.includes(k)?args[args.indexOf(k)+1]:d;
|
||||||
|
const oldTag='1175f1a1d2c1',target=opt('--target','main'),engine=opt('--browser','all');
|
||||||
|
assert.ok(['all','chromium','webkit'].includes(engine));
|
||||||
|
execFileSync('bash',[join(repo,'perf/make-shell-fixture.sh'),'b77938a',oldTag],{cwd:repo,stdio:'inherit'});
|
||||||
|
const scratch=mkdtempSync(join(repo,'perf/.tmp/settings-'));
|
||||||
|
function archive(rev,dest){mkdirSync(dest,{recursive:true});const tar=join(dest,'source.tar');writeFileSync(tar,execFileSync('git',['archive',rev,'frontend','server'],{cwd:repo,maxBuffer:40*1024*1024}));execFileSync('tar',['-xf',tar,'-C',dest]);symlinkSync(join(repo,'server/node_modules'),join(dest,'server/node_modules'));}
|
||||||
|
const revision=execFileSync('git',['rev-parse',target],{cwd:repo,encoding:'utf8'}).trim();
|
||||||
|
archive('b77938a',join(scratch,'old'));archive(revision,join(scratch,'new'));
|
||||||
|
symlinkSync(join(repo,'perf/fixtures/shell-'+oldTag),join(scratch,'old/server/public'));
|
||||||
|
symlinkSync(join(scratch,'new/frontend'),join(scratch,'new/server/public'));
|
||||||
|
const results={revision,oldTag,results:[],failures:[],unsupported:[]};
|
||||||
|
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||||
|
async function poll(fn,timeout=90000){const until=Date.now()+timeout;while(Date.now()<until){if(await fn())return;await sleep(100);}throw Error('Condition timed out');}
|
||||||
|
const ui={saveBeforePlay:['setSaveBeforePlay',true],autoPreload:['setAutoPreload',true],audioOnly:['setAudioOnly',false],quality:['setQuality','360p'],repeatMode:['setRepeat',true],loopOne:['setLoopOne',true],theme:['setTheme','light'],fontScale:['setFont','large'],density:['setDensity','compact'],perfMode:['setPerf',true],reduceMotion:['setMotion',true],parallelSaves:['setParallelSaves','2'],levelVolume:['setLevelVolume',false],layout:['setLayout','f7-swipe']};
|
||||||
|
async function run(name){
|
||||||
|
const cwd=join(scratch,name);mkdirSync(cwd);mkdirSync(join(cwd,'media'));
|
||||||
|
const sock=createServer();await new Promise(r=>sock.listen(0,'127.0.0.1',r));const port=sock.address().port;await new Promise(r=>sock.close(r));let origin='http://127.0.0.1:'+port;
|
||||||
|
let child,browser,proxy,profileReply=null,userDataGets=0,log='';
|
||||||
|
const result={engine:name,errors:[],profileChecks:[],behaviour:[],diff:[]};results.results.push(result);
|
||||||
|
async function stop(){if(!child)return;const c=child;child=null;if(c.exitCode===null){c.kill('SIGTERM');await new Promise(r=>c.once('exit',r));}}
|
||||||
|
async function start(old){symlinkSync(old?join(repo,'perf/fixtures/shell-'+oldTag):join(scratch,'new/frontend'),join(cwd,'public'));child=spawn('bun',[join(scratch,old?'old':'new','server/server.js')],{cwd,env:{...process.env,PORT:String(port),DB_PATH:join(cwd,'db.sqlite'),MEDIA_DIR:join(cwd,'media'),UPLOAD_DIR:join(cwd,'uploads'),DATA_DIR:join(cwd,'data'),ASSET_HASHING:old?'0':'1',ASSET_SYNC:old?'0':'1',BUILD_TAG:''},stdio:['ignore','pipe','pipe']});for(const s of [child.stdout,child.stderr])s.on('data',c=>log=(log+c).slice(-5000));await poll(async()=>{if(child.exitCode!==null)throw Error(log);try{return(await fetch(origin+'/api/version')).ok;}catch{return false;}});}
|
||||||
|
try{
|
||||||
|
await start(true);
|
||||||
|
const clip={id:'benchmedia01',title:'Persisted settings local clip',channel:'Settings probe',duration:3,thumbnail:''};
|
||||||
|
const mp4=join(cwd,'media',clip.id+'.1.mp4');execFileSync('ffmpeg',['-y','-f','lavfi','-i','color=c=blue:s=160x90:r=10:d=3','-f','lavfi','-i','sine=frequency=440:duration=3','-c:v','libx264','-pix_fmt','yuv420p','-c:a','aac','-movflags','+faststart','-shortest',mp4],{stdio:'ignore'});
|
||||||
|
execFileSync('ffmpeg',['-y','-i',mp4,'-vn','-c:a','copy',join(cwd,'media',clip.id+'.1.m4a')],{stdio:'ignore'});
|
||||||
|
const require=createRequire(import.meta.url),{createClient}=require(join(repo,'server/node_modules/@libsql/client'));
|
||||||
|
const db=createClient({url:'file:'+join(cwd,'db.sqlite')});await db.execute({sql:'INSERT OR REPLACE INTO media_cache (video_id,status,gen,size,height,vcodec,acodec,duration,meta,last_access) VALUES (?,?,?,?,?,?,?,?,?,?)',args:[clip.id,'ready',1,statSync(mp4).size,90,'h264','aac',3,JSON.stringify(clip),Date.now()]});db.close();
|
||||||
|
browser=await({chromium,webkit}[name]).launch({headless:true});const context=await browser.newContext({viewport:{width:414,height:896}});
|
||||||
|
// Server-side mocks also cover WebKit worker-owned fetches, which bypass page routing.
|
||||||
|
proxy=createHttpServer((req,res)=>{
|
||||||
|
const pathname=new URL(req.url,'http://local').pathname;
|
||||||
|
if(pathname==='/api/user/data'){userDataGets++;res.setHeader('Content-Type','application/json');res.end(JSON.stringify({ok:true,data:{settings:{saveBeforePlay:false}}}));return;}
|
||||||
|
if(pathname==='/api/profile/load'&&profileReply){res.setHeader('Cache-Control','no-store');res.setHeader('Content-Type','application/json');res.end(JSON.stringify(profileReply));return;}
|
||||||
|
if(pathname==='/api/profile/save'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify({ok:false,error:'probe blocks writes'}));return;}
|
||||||
|
const upstream=httpRequest({hostname:'127.0.0.1',port,path:req.url,method:req.method,headers:req.headers},r=>{res.writeHead(r.statusCode,r.headers);r.pipe(res);});upstream.on('error',()=>{res.statusCode=503;res.end();});req.pipe(upstream);
|
||||||
|
});await new Promise(r=>proxy.listen(0,'127.0.0.1',r));origin='http://127.0.0.1:'+proxy.address().port;
|
||||||
|
await context.route('**/*',route=>route.request().url().startsWith(origin)?route.continue():route.abort());
|
||||||
|
const page=await context.newPage();page.on('pageerror',e=>result.errors.push(e.message));
|
||||||
|
const boot=async()=>{await page.goto(origin);await page.waitForFunction(()=>typeof data!=='undefined'&&document.querySelector('#sidebarToggle'));await page.waitForTimeout(800);};
|
||||||
|
const settings=()=>page.evaluate(()=>JSON.parse(localStorage.getItem('_ytpdata')).settings);
|
||||||
|
const openSettings=async()=>{await page.locator('[data-view="settings"]').first().evaluate(e=>e.click());await page.waitForSelector('#setSaveBeforePlay',{state:'attached'});};
|
||||||
|
const readUI=()=>page.evaluate(ids=>Object.fromEntries(ids.map(id=>{const e=document.getElementById(id);return[id,e.type==='checkbox'?e.checked:e.value];})),Object.values(ui).map(x=>x[0]));
|
||||||
|
await boot();await page.waitForFunction(()=>!!navigator.serviceWorker.controller);
|
||||||
|
await openSettings();
|
||||||
|
for(const [key,[id,value]]of Object.entries(ui)){await page.locator('#'+id).evaluate((e,value)=>{if(e.type==='checkbox')e.checked=value;else e.value=value;e.dispatchEvent(new Event('change',{bubbles:true}));},value);}
|
||||||
|
await page.evaluate(()=>EQ.open());await page.waitForSelector('[data-preset="bass"]',{state:'attached'});await page.locator('[data-preset="bass"]').click();await page.evaluate(()=>closeModal());await sleep(600);
|
||||||
|
await boot();await openSettings();result.old=await settings();result.oldUI=await readUI();result.oldRaw=await page.evaluate(()=>localStorage.getItem('_ytpdata'));
|
||||||
|
async function behaviour(stage){const supported=await page.evaluate(()=>OPFS.isSupported());if(!supported){result.behaviour.push({stage,supported:false,note:'Native OPFS unavailable; no shim'});results.unsupported.push(name);return;}
|
||||||
|
await page.evaluate(async id=>{await OPFS.deleteVideo(id);cachedIds.delete(id);Player.pause();},clip.id);
|
||||||
|
await page.evaluate(clip=>Player.loadVideo(clip),clip);await page.waitForFunction(id=>cachedIds.has(id)&&!!current?.localUrl,clip.id,{timeout:20000});
|
||||||
|
const state=await page.evaluate(async id=>({cached:cachedIds.has(id),files:await OPFS.listVideos(),localUrl:current.localUrl,src:Player.master?.src}),clip.id);assert.equal(state.files.find(f=>f.id===clip.id)?.size,statSync(mp4).size);assert.ok(state.src?.startsWith('blob:'),'player must use OPFS blob');await page.waitForFunction(()=>Player.master?.readyState>=3);await page.evaluate(()=>Player.play());await page.waitForFunction(()=>Player.master?.currentTime>0);await page.evaluate(()=>Player.pause());result.behaviour.push({stage,supported:true,bytes:state.files.find(f=>f.id===clip.id).size,cached:true,localPlayback:true});}
|
||||||
|
await behaviour('old');console.log(name,'old settings and save verified');await page.evaluate(()=>Player.pause());
|
||||||
|
await stop();unlinkSync(join(cwd,'public'));await start(false);result.newTag=(await(await fetch(origin+'/api/version')).json()).buildTag;
|
||||||
|
await page.evaluate(async()=>{await checkBuildTag();await(await navigator.serviceWorker.getRegistration()).update();});await poll(()=>page.evaluate(async()=>!!(await navigator.serviceWorker.getRegistration())?.waiting));
|
||||||
|
await page.waitForFunction(()=>!document.getElementById('modal').classList.contains('hidden')&&/Update available/.test(document.getElementById('modalTitle').textContent));
|
||||||
|
await page.locator('#modal').getByRole('button',{name:'Refresh UI',exact:true}).evaluate(e=>e.click());await page.waitForFunction(tag=>document.querySelector('meta[name="ytp-build"]')?.content===tag,result.newTag,{timeout:90000});await sleep(1000);await openSettings();
|
||||||
|
console.log(name,'real Refresh UI completed');result.upgraded=await settings();result.newRaw=await page.evaluate(()=>localStorage.getItem('_ytpdata'));result.newUI=await readUI();assert.deepEqual(result.newUI,result.oldUI,'checkbox/select values preserved');
|
||||||
|
const {migrate}=require(join(scratch,'new/frontend/eq-parameters.js'));const expected=structuredClone(result.old);if(expected.eq?.g?.length===5)expected.eq.g=migrate(expected.eq.g);
|
||||||
|
for(const key of new Set([...Object.keys(result.old),...Object.keys(result.upgraded)]))result.diff.push({key,before:result.old[key],after:result.upgraded[key],equal:JSON.stringify(result.old[key])===JSON.stringify(result.upgraded[key]),expectedMigration:key==='eq'&&result.old.eq?.g?.length===5});
|
||||||
|
assert.deepEqual(result.upgraded,expected,'all persisted settings preserved except deliberate 5→10 EQ migration');await behaviour('upgraded');
|
||||||
|
const local=await settings(),remote={...local,saveBeforePlay:false,autoPreload:false,layout:'classic',quality:'auto'};
|
||||||
|
async function profileCheck(updatedAt,expectedSave,stage){profileReply={ok:true,name:'probe',updatedAt,data:{settings:remote,playlists:[]}};await page.evaluate(s=>{data.settings=s;data.profile={name:'probe',syncedAt:200};localStorage.setItem('_ytpdata',JSON.stringify(data));},local);await boot();await page.waitForFunction(()=>_syncBusy===0);await openSettings();const s=await settings();assert.equal(s.saveBeforePlay,expectedSave);assert.equal(await page.locator('#setSaveBeforePlay').isChecked(),expectedSave);if(!expectedSave)assert.equal(s.layout,'classic');else assert.deepEqual(s,local);result.profileChecks.push({stage,serverUpdatedAt:updatedAt,localSyncedAt:200,saveBeforePlay:s.saveBeforePlay});}
|
||||||
|
await profileCheck(100,true,'older timestamp ignored');await profileCheck(300,false,'older settings with newer timestamp adopted');
|
||||||
|
profileReply=null;await page.evaluate(s=>{data.profile=null;data.settings=s;localStorage.setItem('_ytpdata',JSON.stringify(data));},local);await boot();await openSettings();
|
||||||
|
await page.locator('#backupFileInput').setInputFiles({name:'settings-probe.json',mimeType:'application/json',buffer:Buffer.from(JSON.stringify({playlists:[],settings:remote}))});await page.waitForFunction(()=>document.getElementById('modalTitle').textContent==='Import backup?');await page.locator('#modal').getByRole('button',{name:'Import',exact:true}).evaluate(e=>e.click());await sleep(600);assert.deepEqual(await settings(),local,'backup import does not replace settings');result.importPreserved=true;result.userDataGets=userDataGets;assert.equal(userDataGets,0,'boot does not pull settings from fingerprint user-data endpoint');
|
||||||
|
console.log(name,JSON.stringify({keys:result.diff.length,changed:result.diff.filter(x=>!x.equal),behaviour:result.behaviour,profileChecks:result.profileChecks,importPreserved:result.importPreserved,errors:result.errors}));
|
||||||
|
}finally{await browser?.close();await stop();if(proxy){proxy.closeAllConnections();await new Promise(r=>proxy.close(r));}}
|
||||||
|
}
|
||||||
|
try{for(const name of ['chromium','webkit'])if(engine==='all'||engine===name){try{await run(name);}catch(e){results.failures.push({engine:name,error:e.stack});console.error(name,e.message);}}}finally{const out=resolve(opt('--out','perf/results/settings-persist.json'));mkdirSync(resolve(out,'..'),{recursive:true});writeFileSync(out,JSON.stringify(results,null,2)+'\n');rmSync(scratch,{recursive:true,force:true});}
|
||||||
|
if(results.failures.length)process.exitCode=1;else if(results.unsupported.length&&!args.includes('--allow-unsupported-webkit')){console.error('Native WebKit OPFS unverified; use --allow-unsupported-webkit for persistence/UI coverage');process.exitCode=2;}
|
||||||
131
plans/settings-persist-investigation.md
Normal file
131
plans/settings-persist-investigation.md
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# Persisted settings across production updates
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
No Phase 1–5 persistence regression was reproduced. No runtime fix or breaking
|
||||||
|
commit is justified. The old b77938a shell (verified tag `1175f1a1d2c1`) upgraded
|
||||||
|
through its own banner/Refresh UI flow at the same origin to current main
|
||||||
|
`cf27d4f15a29747123437862ab4e461fd78178e6`. All 24 existing setting keys and all 14 checked controls were preserved
|
||||||
|
on Chromium and WebKit. The original worktree base/live deployment snapshot
|
||||||
|
`0b8e398` was also tested separately: both engines preserve the same 24/24 keys,
|
||||||
|
controls, profile/import policy; Chromium again saves and plays the complete
|
||||||
|
clip before/after. This avoids conflating concurrent main changes.
|
||||||
|
|
||||||
|
Both servers are actual archived Bun implementations, with isolated DB/media
|
||||||
|
directories. A local proxy supplies deliberately older profile copies and blocks
|
||||||
|
profile writes; no credentials or real online profiles are used. Settings values
|
||||||
|
were set using real controls/change handlers, and EQ using its actual preset
|
||||||
|
button. The installed page was reloaded before recording its exact `_ytpdata`
|
||||||
|
JSON; old/new raw snapshots are in the results file. Refresh UI and import use
|
||||||
|
the existing button handlers via `.click()` on the DOM element: physical
|
||||||
|
Playwright hit-testing was obstructed by the bottom nav at large font size.
|
||||||
|
This tests persistence/update handlers, not modal pointer geometry.
|
||||||
|
|
||||||
|
## Every-key comparison
|
||||||
|
|
||||||
|
Values below are identical on both engines; no key was removed or reset.
|
||||||
|
|
||||||
|
| Key | Old (after reload) | Upgraded | Status |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| quality | `"360p"` | `"360p"` | same |
|
||||||
|
| volume | `1` | `1` | same |
|
||||||
|
| audioOnly | `false` | `false` | same |
|
||||||
|
| autoPreload | `true` | `true` | same |
|
||||||
|
| p2pShare | `true` | `true` | same |
|
||||||
|
| p2pReceive | `true` | `true` | same |
|
||||||
|
| saveBeforePlay | `true` | `true` | same |
|
||||||
|
| repeatMode | `"all"` | `"all"` | same |
|
||||||
|
| loopOne | `true` | `true` | same |
|
||||||
|
| shuffle | `false` | `false` | same |
|
||||||
|
| theme | `"light"` | `"light"` | same |
|
||||||
|
| fontScale | `"large"` | `"large"` | same |
|
||||||
|
| levelVolume | `false` | `false` | same |
|
||||||
|
| liveAudioIOS | `false` | `false` | same |
|
||||||
|
| parallelSaves | `2` | `2` | same |
|
||||||
|
| density | `"compact"` | `"compact"` | same |
|
||||||
|
| perfMode | `true` | `true` | same |
|
||||||
|
| reduceMotion | `true` | `true` | same |
|
||||||
|
| layout | `"f7-swipe"` | `"f7-swipe"` | same |
|
||||||
|
| autoBackupEnabled | `false` | `false` | same |
|
||||||
|
| autoBackupIntervalDays | `7` | `7` | same |
|
||||||
|
| serviceVideo | `false` | `false` | same |
|
||||||
|
| foldedCards | `{}` | `{}` | same |
|
||||||
|
| eq | `{"preset":"bass","g":[5,7,5,3,1,0,0,0,0,0]}` | `{"preset":"bass","g":[5,7,5,3,1,0,0,0,0,0]}` | same |
|
||||||
|
|
||||||
|
Save before playing and Auto-save playlist videos stayed ON; audio-only OFF.
|
||||||
|
Quality stayed 360p, layout f7-swipe, loop/repeat ON, light theme, large font,
|
||||||
|
compact density, performance/reduced motion ON and two parallel saves.
|
||||||
|
The tested b77938a build already has ten-band EQ, so its Bass boost curve was
|
||||||
|
unchanged. The five-band compatibility migration in app.js was not triggered.
|
||||||
|
|
||||||
|
## Behaviour and alternative overwrite paths
|
||||||
|
|
||||||
|
Chromium saved the complete 30,290-byte H.264/AAC clip before and after the
|
||||||
|
upgrade, set cachedIds, used an OPFS blob source, reached readyState ≥3 and
|
||||||
|
advanced playback time. The existing copy was deleted before the second open
|
||||||
|
to force a genuinely new post-upgrade download. Linux WebKit reports native
|
||||||
|
OPFS unsupported on both builds; preservation/UI checks pass but successful
|
||||||
|
native storage/playback there is not claimed or shimmed.
|
||||||
|
|
||||||
|
Profile tests exercise the launch path with `syncedAt:200`:
|
||||||
|
|
||||||
|
| Server updatedAt | Server Save before playing | Device result, both engines |
|
||||||
|
| ---: | --- | --- |
|
||||||
|
| 100 | false | Local true and every local setting retained |
|
||||||
|
| 300 | false | Server false adopted; layout classic and defaults/remote values applied |
|
||||||
|
|
||||||
|
A copy with old *values* but a newer timestamp can therefore disable autosave.
|
||||||
|
This is existing last-write-wins behavior, not an update migration regression:
|
||||||
|
`pullProfileIfNewer()` and `applyProfileData()` at b77938a already implement the
|
||||||
|
same timestamp comparison and whole-settings replacement. Equal timestamps
|
||||||
|
also retain local settings (unit test). A genuine older timestamp cannot reset
|
||||||
|
local settings in these tests.
|
||||||
|
|
||||||
|
`API.loadData()` reads localStorage in web mode; boot overlays loaded.settings
|
||||||
|
on DEFAULT_SETTINGS. Phase 4 extraction does not create a second data object.
|
||||||
|
The Phase 3 head loader only reads `_ytpdata` and sets appearance attributes;
|
||||||
|
it never writes settings. `syncOnLaunch()` invokes profile reconciliation
|
||||||
|
after the first render. `/api/user/data` was monitored/mocked with false
|
||||||
|
autosave settings and received zero requests: fingerprint sync is not a settings
|
||||||
|
pull at launch. `saveDataToStorage()` pushes metadata to `/api/user/sync`,
|
||||||
|
without a settings field.
|
||||||
|
|
||||||
|
Importing a backup containing opposite settings through the actual file input
|
||||||
|
and Import button preserved the current settings. `importBackup()` merges
|
||||||
|
playlists/history/positions but does not apply `imp.settings`. Intentional
|
||||||
|
profile adoption/share-link loading uses `applyProfileData()` and *does* replace
|
||||||
|
settings; this is already documented on the existing confirmation UI.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node perf/settings-persist.mjs --browser all --target main --allow-unsupported-webkit
|
||||||
|
node perf/settings-persist.mjs --browser all --target 0b8e398 --allow-unsupported-webkit --out perf/results/settings-persist-live-base.json
|
||||||
|
node --test frontend/*.test.js
|
||||||
|
(cd server && bun install && bun run test)
|
||||||
|
```
|
||||||
|
|
||||||
|
Without `--allow-unsupported-webkit`, unverified native WebKit storage exits 2.
|
||||||
|
Any assertion failure exits 1 even with that allowance. The server-side proxy
|
||||||
|
is essential: WebKit worker network requests bypass page-route mocks.
|
||||||
|
|
||||||
|
The two shared Chromium Settings errors (`null.addEventListener`) are recorded
|
||||||
|
in the results; they already occur on the old build. They did not stop the
|
||||||
|
playback/autosave handlers from persisting or saving. This is not an assertion
|
||||||
|
that every Settings interaction is error-free.
|
||||||
|
|
||||||
|
Only tests, harness and documentation changed; no runtime implementation or
|
||||||
|
save behavior changed. A phase bisect and failing-then-fixed runtime test cannot be supplied
|
||||||
|
honestly because no setting loss was reproduced on either target.
|
||||||
|
|
||||||
|
## Phone checks
|
||||||
|
|
||||||
|
On the installed PWA, inspect Settings → Playback → Save before playing, ensure
|
||||||
|
Audio-only is OFF and no explicit streaming override is selected. Auto-save
|
||||||
|
playlist videos is a separate policy. Check the linked profile and whether
|
||||||
|
another device/imported profile pushed a newer copy with the toggle OFF.
|
||||||
|
Compare settings before/after an online launch. Re-check origin/PWA identity,
|
||||||
|
iOS Clear Website Data/storage eviction/private mode, free quota and Downloads
|
||||||
|
failure details. Native iPhone OPFS, suspension, quota sharing and storage reset
|
||||||
|
remain unverified by Linux WebKit. A phone-side failing sequence/settings export
|
||||||
|
is needed before changing the sync policy or save logic.
|
||||||
Reference in New Issue
Block a user