Skip to content

Commit

Permalink
chore: Logic to deal with invalid track to playlist references.
Browse files Browse the repository at this point in the history
- We also call this function whenever a crash gets caught (ie: error boundary).
  • Loading branch information
cyanChill committed Jan 15, 2025
1 parent 4940209 commit c153125
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 3 deletions.
9 changes: 8 additions & 1 deletion mobile/src/api/playlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,13 @@ export async function deletePlaylist(id: string) {
/** Replace the "junction" field from the `Playlist` table with `tracks`. */
function fixPlaylistJunction(data: PlaylistWithJunction): PlaylistWithTracks {
const { tracksToPlaylists, ...rest } = data;
return { ...rest, tracks: tracksToPlaylists.map(({ track }) => track) };
return {
...rest,
// Note: We do the filter in the case where we attempted to delete a track,
// but failed to do so (resulting in an invalid track floating around).
tracks: tracksToPlaylists
.map(({ track }) => track)
.filter((t) => t !== null),
};
}
//#endregion
2 changes: 2 additions & 0 deletions mobile/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { AppProvider } from "@/providers";
import "@/resources/global.css";
import "@/modules/i18n"; // Make sure translations are bundled.
import { TopAppBar, TopAppBarMarquee } from "@/components/TopAppBar";
import { MigrationFunctionMap } from "@/modules/scanning/helpers/migrations";

// Catch any errors thrown by the Layout component.
export { ErrorBoundary };
Expand Down Expand Up @@ -40,6 +41,7 @@ export default function RootLayout() {
try {
// Clear music store state in case error propagated from invalid data.
musicStore.getState().reset();
MigrationFunctionMap["no-track-playlist-ref"]();
} catch {}
// Send error message to Sentry. Doesn't send if you followed the
// "Personal Privacy Build" documentation.
Expand Down
7 changes: 6 additions & 1 deletion mobile/src/modules/scanning/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
const MigrationOptions = ["v1-to-v2-store", "v1-to-v2-schema"] as const;
const MigrationOptions = [
"v1-to-v2-store",
"v1-to-v2-schema",
"no-track-playlist-ref",
] as const;

export type MigrationOption = (typeof MigrationOptions)[number];

Expand All @@ -24,4 +28,5 @@ export const MigrationHistory: Record<
version: "v2.0.0-rc.1",
changes: ["v1-to-v2-store", "v1-to-v2-schema"],
},
4: { version: "v2.0.1", changes: ["no-track-playlist-ref"] },
};
23 changes: 22 additions & 1 deletion mobile/src/modules/scanning/helpers/migrations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AsyncStorage from "@react-native-async-storage/async-storage";
import { eq } from "drizzle-orm";
import { eq, inArray } from "drizzle-orm";

import { db } from "@/db";
import { tracks, tracksToPlaylists } from "@/db/schema";
Expand Down Expand Up @@ -79,6 +79,27 @@ export const MigrationFunctionMap: Record<
}
});
},
/** Removes track to playlist relations where the track doesn't exist. */
"no-track-playlist-ref": async () => {
const [allTracks, trackRels] = await Promise.all([
db.query.tracks.findMany({ columns: { id: true } }),
db
.selectDistinct({ id: tracksToPlaylists.trackId })
.from(tracksToPlaylists),
]);
try {
const trackIds = new Set(allTracks.map((t) => t.id));
const relTrackIds = trackRels.map((t) => t.id);
// Get ids in the track to playlist relationship where the track id
// doesn't exist and delete them.
const invalidTracks = relTrackIds.filter((id) => !trackIds.has(id));
if (invalidTracks.length > 0) {
await db
.delete(tracksToPlaylists)
.where(inArray(tracksToPlaylists.trackId, invalidTracks));
}
} catch {}
},
};

/** Helper to parse value from AsyncStorage. */
Expand Down
2 changes: 2 additions & 0 deletions mobile/src/screens/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import { AppProvider } from "@/providers";

import { Card } from "@/components/Containment/Card";
import { StyledText } from "@/components/Typography/StyledText";
import { MigrationFunctionMap } from "@/modules/scanning/helpers/migrations";

/** Screen displayed when an error is thrown in a component. */
export function ErrorBoundary({ error }: ErrorBoundaryProps) {
useEffect(() => {
try {
// Clear music store state in case error propagated from invalid data.
musicStore.getState().reset();
MigrationFunctionMap["no-track-playlist-ref"]();
} catch {}
}, []);

Expand Down

0 comments on commit c153125

Please sign in to comment.