318 lines
9.7 KiB
JavaScript
318 lines
9.7 KiB
JavaScript
import { localStorageAPI, STORAGE_KEYS } from "./localStorage";
|
|
|
|
// Migration utility to copy localStorage data to backend
|
|
export async function migrateLocalStorageToBackend(customBase) {
|
|
// Determine API base dynamically from saved settings unless overridden
|
|
const apiSettings = (() => {
|
|
try {
|
|
return JSON.parse(localStorage.getItem("api_settings") || "{}");
|
|
} catch {
|
|
return {};
|
|
}
|
|
})();
|
|
|
|
const API_BASE = (() => {
|
|
if (customBase) return customBase;
|
|
if (apiSettings.protocol && apiSettings.hostname) {
|
|
const p = apiSettings.port || "";
|
|
const portPart = p ? `:${p}` : "";
|
|
return `${apiSettings.protocol}://${apiSettings.hostname}${portPart}/api`;
|
|
}
|
|
return `https://houseofprayer.ddns.net:8080/api`;
|
|
})();
|
|
|
|
console.log("[Migration] Upload API Base:", API_BASE);
|
|
|
|
try {
|
|
// Preload existing backend data to minimize duplicates
|
|
let existingSongs = [];
|
|
let existingProfiles = [];
|
|
let existingPlans = [];
|
|
try {
|
|
console.log("[Migration] Fetching existing backend data...");
|
|
const [songsRes, profilesRes, plansRes] = await Promise.all([
|
|
fetch(`${API_BASE}/songs`).then((r) => (r.ok ? r.json() : [])),
|
|
fetch(`${API_BASE}/profiles`).then((r) => (r.ok ? r.json() : [])),
|
|
fetch(`${API_BASE}/plans`).then((r) => (r.ok ? r.json() : [])),
|
|
]);
|
|
existingSongs = songsRes;
|
|
existingProfiles = profilesRes;
|
|
existingPlans = plansRes;
|
|
} catch (e) {}
|
|
|
|
const existingSongTitles = new Set(
|
|
existingSongs.map((s) => (s.title || "").toLowerCase())
|
|
);
|
|
const existingProfileIds = new Set(existingProfiles.map((p) => p.id));
|
|
const existingPlanDates = new Set(existingPlans.map((pl) => pl.date));
|
|
|
|
// 1. Migrate Songs
|
|
const songs = await localStorageAPI.getSongs("");
|
|
for (const song of songs) {
|
|
if (existingSongTitles.has((song.title || "").toLowerCase())) {
|
|
continue;
|
|
}
|
|
try {
|
|
const res = await fetch(`${API_BASE}/songs`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(song),
|
|
});
|
|
if (res.ok) {
|
|
} else {
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
// 2. Migrate Profiles
|
|
const profiles = await localStorageAPI.getProfiles();
|
|
for (const profile of profiles) {
|
|
if (existingProfileIds.has(profile.id)) {
|
|
continue;
|
|
}
|
|
try {
|
|
const res = await fetch(`${API_BASE}/profiles`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(profile),
|
|
});
|
|
if (res.ok) {
|
|
} else {
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
// 3. Migrate Plans
|
|
const plans = await localStorageAPI.getPlans();
|
|
for (const plan of plans) {
|
|
if (existingPlanDates.has(plan.date)) {
|
|
continue;
|
|
}
|
|
try {
|
|
const planSongs = await localStorageAPI.getPlanSongs(plan.id);
|
|
const songsData = [];
|
|
for (const ps of planSongs) {
|
|
const song = await localStorageAPI.getSong(ps.song_id);
|
|
if (song) songsData.push({ ...song, order: ps.order_index });
|
|
}
|
|
const res = await fetch(`${API_BASE}/plans`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ ...plan, songs: songsData }),
|
|
});
|
|
if (res.ok) {
|
|
} else {
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
// 4. Migrate Profile Songs
|
|
const allProfileSongs = JSON.parse(
|
|
localStorage.getItem(STORAGE_KEYS.PROFILE_SONGS) || "[]"
|
|
);
|
|
for (const ps of allProfileSongs) {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/profiles/${ps.profile_id}/songs`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ song_id: ps.song_id }),
|
|
});
|
|
if (res.ok) {
|
|
} else {
|
|
}
|
|
} catch (err) {
|
|
console.error(
|
|
`❌ Failed to migrate profile-song (profile ${ps.profile_id} song ${ps.song_id}):`,
|
|
err
|
|
);
|
|
}
|
|
}
|
|
|
|
// 5. Migrate Profile Song Keys (array form in localStorage)
|
|
const profileSongKeysArr = JSON.parse(
|
|
localStorage.getItem(STORAGE_KEYS.PROFILE_SONG_KEYS) || "[]"
|
|
);
|
|
for (const entry of profileSongKeysArr) {
|
|
if (!entry.profile_id || !entry.song_id || !entry.key) continue;
|
|
try {
|
|
const res = await fetch(
|
|
`${API_BASE}/profiles/${entry.profile_id}/songs/${entry.song_id}/key`,
|
|
{
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ key: entry.key }),
|
|
}
|
|
);
|
|
if (res.ok) {
|
|
console.log(
|
|
`✅ Migrated key for profile ${entry.profile_id} song ${entry.song_id}`
|
|
);
|
|
} else {
|
|
console.warn(
|
|
`⚠️ Failed to migrate key for profile ${entry.profile_id} song ${entry.song_id}`
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error(
|
|
`❌ Failed to migrate key (profile ${entry.profile_id} song ${entry.song_id}):`,
|
|
err
|
|
);
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
migrated: {
|
|
songs: songs.length,
|
|
profiles: profiles.length,
|
|
plans: plans.length,
|
|
profileSongs: allProfileSongs.length,
|
|
keys: profileSongKeysArr.length,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// Sync FROM backend TO localStorage (pull down all data)
|
|
export async function syncFromBackendToLocalStorage(customBase) {
|
|
const apiSettings = (() => {
|
|
try {
|
|
return JSON.parse(localStorage.getItem("api_settings") || "{}");
|
|
} catch {
|
|
return {};
|
|
}
|
|
})();
|
|
|
|
const API_BASE = (() => {
|
|
if (customBase) return customBase;
|
|
if (apiSettings.protocol && apiSettings.hostname) {
|
|
const p = apiSettings.port || "";
|
|
const portPart = p ? `:${p}` : "";
|
|
return `${apiSettings.protocol}://${apiSettings.hostname}${portPart}/api`;
|
|
}
|
|
return `https://houseofprayer.ddns.net:8080/api`;
|
|
})();
|
|
|
|
console.log("[Migration] Download API Base:", API_BASE);
|
|
|
|
try {
|
|
// 1. Sync Songs
|
|
const songsRes = await fetch(`${API_BASE}/songs`);
|
|
if (!songsRes.ok)
|
|
throw new Error(`Failed to fetch songs: ${songsRes.status}`);
|
|
const backendSongs = await songsRes.json();
|
|
|
|
const localSongs = await localStorageAPI.getSongs("");
|
|
const localSongIds = new Set(localSongs.map((s) => s.id));
|
|
|
|
let songsAdded = 0;
|
|
let songsUpdated = 0;
|
|
|
|
for (const song of backendSongs) {
|
|
// Fetch full song details
|
|
try {
|
|
const detailRes = await fetch(`${API_BASE}/songs/${song.id}`);
|
|
if (!detailRes.ok) {
|
|
continue;
|
|
}
|
|
const fullSong = await detailRes.json();
|
|
|
|
if (!fullSong.lyrics && !fullSong.chords) {
|
|
}
|
|
|
|
if (localSongIds.has(fullSong.id)) {
|
|
await localStorageAPI.updateSong(fullSong.id, fullSong);
|
|
songsUpdated++;
|
|
} else {
|
|
await localStorageAPI.createSong(fullSong);
|
|
songsAdded++;
|
|
}
|
|
} catch (err) {}
|
|
}
|
|
|
|
// 2. Sync Profiles
|
|
const profilesRes = await fetch(`${API_BASE}/profiles`);
|
|
if (!profilesRes.ok)
|
|
throw new Error(`Failed to fetch profiles: ${profilesRes.status}`);
|
|
const backendProfiles = await profilesRes.json();
|
|
|
|
const localProfiles = await localStorageAPI.getProfiles();
|
|
const localProfileIds = new Set(localProfiles.map((p) => p.id));
|
|
|
|
let profilesAdded = 0;
|
|
let profilesUpdated = 0;
|
|
|
|
for (const profile of backendProfiles) {
|
|
if (localProfileIds.has(profile.id)) {
|
|
await localStorageAPI.updateProfile(profile.id, profile);
|
|
profilesUpdated++;
|
|
} else {
|
|
await localStorageAPI.createProfile(profile);
|
|
profilesAdded++;
|
|
}
|
|
}
|
|
|
|
// 3. Sync Plans
|
|
const plansRes = await fetch(`${API_BASE}/plans`);
|
|
if (!plansRes.ok)
|
|
throw new Error(`Failed to fetch plans: ${plansRes.status}`);
|
|
const backendPlans = await plansRes.json();
|
|
|
|
const localPlans = await localStorageAPI.getPlans();
|
|
const localPlanIds = new Set(localPlans.map((p) => p.id));
|
|
|
|
let plansAdded = 0;
|
|
let plansUpdated = 0;
|
|
|
|
for (const plan of backendPlans) {
|
|
if (localPlanIds.has(plan.id)) {
|
|
await localStorageAPI.updatePlan(plan.id, plan);
|
|
plansUpdated++;
|
|
} else {
|
|
await localStorageAPI.createPlan(plan);
|
|
plansAdded++;
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
synced: {
|
|
songs: { added: songsAdded, updated: songsUpdated },
|
|
profiles: { added: profilesAdded, updated: profilesUpdated },
|
|
plans: { added: plansAdded, updated: plansUpdated },
|
|
},
|
|
};
|
|
} catch (error) {
|
|
return { success: false, error: error.message };
|
|
}
|
|
}
|
|
|
|
// Full bidirectional sync: merge localStorage and backend data
|
|
export async function fullSync(customBase) {
|
|
// First, push local changes to backend
|
|
const uploadResult = await migrateLocalStorageToBackend(customBase);
|
|
|
|
// Then, pull backend changes to local
|
|
const downloadResult = await syncFromBackendToLocalStorage(customBase);
|
|
|
|
// Trigger page refresh to show updated data without full reload
|
|
window.dispatchEvent(new CustomEvent("dataFullySynced"));
|
|
|
|
// Also trigger events that the app already listens for
|
|
window.dispatchEvent(new Event("songsChanged"));
|
|
window.dispatchEvent(new Event("plansChanged"));
|
|
|
|
return {
|
|
success: uploadResult.success && downloadResult.success,
|
|
upload: uploadResult,
|
|
download: downloadResult,
|
|
};
|
|
}
|
|
|
|
// Export functions to call from browser console
|
|
window.migrateData = migrateLocalStorageToBackend;
|
|
window.syncFromBackend = syncFromBackendToLocalStorage;
|
|
window.fullSync = fullSync;
|