Skip to content

Commit 00ecba7

Browse files
committed
Deduplication
1 parent 640a49e commit 00ecba7

3 files changed

Lines changed: 101 additions & 57 deletions

File tree

Build/src/helpers/filePickerHelper.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -834,10 +834,9 @@ export function handleShareTarget(): ShareTargetResult | null {
834834
return null; // Will be handled in useShareTarget
835835
}
836836

837-
export function useShareTarget(
838-
onShareReceived: (result: ShareTargetResult) => void
839-
) {
837+
export function useShareTarget(onShareReceived: (result: ShareTargetResult) => void) {
840838
const hasProcessedRef = useRef(false);
839+
const processedFilesRef = useRef<Set<string>>(new Set()); // Track processed file IDs
841840

842841
useEffect(() => {
843842
if (hasProcessedRef.current) return;
@@ -855,9 +854,14 @@ export function useShareTarget(
855854
if (key.url.includes("/shared-file-")) {
856855
const response = await cache.match(key);
857856
const blob = await response?.blob();
858-
const fileName =
859-
response?.headers.get("x-file-name") || "unknown-file";
860-
files.push(new File([blob!], fileName, { type: blob?.type }));
857+
const fileName = response?.headers.get("x-file-name") || "unknown-file";
858+
859+
// Generate a unique ID for each file (name + size + lastModified)
860+
const id = `${fileName}-${blob?.size}`;
861+
if (!processedFilesRef.current.has(id)) {
862+
processedFilesRef.current.add(id); // Mark as processed
863+
files.push(new File([blob!], fileName, { type: blob?.type }));
864+
}
861865
}
862866
}
863867

@@ -880,7 +884,6 @@ export function useShareTarget(
880884
}
881885
}
882886

883-
// Fallback to text sharing (e.g., query parameters)
884887
const shareResult = handleShareTarget();
885888
if (shareResult) {
886889
hasProcessedRef.current = true;

Build/src/helpers/importAudioFiles.tsx

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,41 @@ import { setAlbumArtInCache } from "../hooks/useAlbumArt";
66
export async function importAudioFiles(
77
audioFiles: Array<{ file: File } | File>,
88
addSong: (song: Song, file: File) => Promise<void>,
9-
t: any,
9+
t: any
1010
) {
1111
if (!audioFiles || audioFiles.length === 0) return;
1212

1313
const BATCH_SIZE = 10;
1414
let successCount = 0;
1515
let errorCount = 0;
16+
let skippedCount = 0; // Count skipped duplicates
1617
let currentBatch = 1;
1718
const totalBatches = Math.ceil(audioFiles.length / BATCH_SIZE);
1819

20+
// Track unique files to avoid duplicate imports in this session
21+
const processedFileIds = new Set<string>();
22+
1923
for (let i = 0; i < audioFiles.length; i += BATCH_SIZE) {
2024
const batch = audioFiles.slice(i, i + BATCH_SIZE);
21-
toast.loading(t("batch.processing", { currentBatch, totalBatches }));
25+
toast.loading(
26+
t("batch.processing", { currentBatch, totalBatches, successCount })
27+
);
2228

2329
// Process sequentially to reduce memory pressure
2430
for (const audioFile of batch) {
2531
try {
2632
const file: File = (audioFile as any).file || (audioFile as File);
33+
34+
// Unique ID based on file metadata (name, size, lastModified)
35+
const fileId = `${file.name}-${file.size}-${file.lastModified}`;
36+
if (processedFileIds.has(fileId)) {
37+
// Skip duplicates detected via unique ID
38+
console.log(`Skipping duplicate file: ${file.name}`);
39+
skippedCount++;
40+
continue;
41+
}
42+
processedFileIds.add(fileId); // Mark the file as processed
43+
2744
const metadata = await extractAudioMetadata(file);
2845
const songId = generateUniqueId();
2946

@@ -32,31 +49,36 @@ export async function importAudioFiles(
3249
let processedMimeType = file.type;
3350

3451
const isFlo =
35-
file.name.toLowerCase().endsWith('.flo') ||
36-
metadata.encoding?.codec === 'flo';
52+
file.name.toLowerCase().endsWith(".flo") ||
53+
metadata.encoding?.codec === "flo";
3754

3855
if (isFlo) {
3956
try {
4057
const arrayBuffer = await file.arrayBuffer();
41-
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
58+
const isSafari = /^((?!chrome|android).)*safari/i.test(
59+
navigator.userAgent
60+
);
4261

4362
if (isSafari) {
4463
// Safari: Pre-decode to WAV for compatibility
4564
const { decodeFloToWav } = await import("./refloWavHelper");
4665
const wavBytes = await decodeFloToWav(arrayBuffer);
47-
const wavBlob = new Blob([wavBytes], { type: 'audio/wav' });
66+
const wavBlob = new Blob([wavBytes], { type: "audio/wav" });
4867
processedFile = new File(
4968
[wavBlob],
50-
file.name.replace(/\.flo$/i, '.wav'),
51-
{ type: 'audio/wav' }
69+
file.name.replace(/\.flo$/i, ".wav"),
70+
{ type: "audio/wav" }
5271
);
53-
processedMimeType = 'audio/wav';
72+
processedMimeType = "audio/wav";
5473
console.log(`Pre-decoded flo to WAV for Safari: ${file.name}`);
5574
} else {
5675
// Non-Safari: Pre-decode to PCM for Web Audio API
5776
const { decodeFloToAudioBuffer } = await import("./floProcessor");
5877
const audioContext = new AudioContext();
59-
const audioBuffer = await decodeFloToAudioBuffer(arrayBuffer, audioContext);
78+
const audioBuffer = await decodeFloToAudioBuffer(
79+
arrayBuffer,
80+
audioContext
81+
);
6082

6183
// Store as interleaved Float32Array PCM
6284
const frameCount = audioBuffer.length;
@@ -66,25 +88,27 @@ export async function importAudioFiles(
6688
// Interleave channels
6789
for (let i = 0; i < frameCount; i++) {
6890
for (let ch = 0; ch < channels; ch++) {
69-
pcmData[i * channels + ch] = audioBuffer.getChannelData(ch)[i];
91+
pcmData[i * channels + ch] = audioBuffer.getChannelData(ch)[
92+
i
93+
];
7094
}
7195
}
7296

73-
const pcmBlob = new Blob([pcmData.buffer], { type: 'audio/pcm' });
97+
const pcmBlob = new Blob([pcmData.buffer], { type: "audio/pcm" });
7498
processedFile = new File(
7599
[pcmBlob],
76-
file.name.replace(/\.flo$/i, '.pcm'),
77-
{ type: 'audio/pcm' }
100+
file.name.replace(/\.flo$/i, ".pcm"),
101+
{ type: "audio/pcm" }
78102
);
79-
processedMimeType = 'audio/pcm';
103+
processedMimeType = "audio/pcm";
80104

81105
// Store AudioBuffer properties for reconstruction
82106
metadata.encoding = {
83107
...metadata.encoding,
84108
sampleRate: audioBuffer.sampleRate,
85109
channels: audioBuffer.numberOfChannels,
86110
bitsPerSample: 32, // Float32
87-
codec: 'pcm-float32',
111+
codec: "pcm-float32",
88112
};
89113

90114
// Close the temporary AudioContext
@@ -95,22 +119,27 @@ export async function importAudioFiles(
95119
} catch (error) {
96120
console.warn("Failed to pre-decode flo file, storing original:", error);
97121
// Keep original file if pre-decoding fails
98-
processedMimeType = 'audio/x-flo';
122+
processedMimeType = "audio/x-flo";
99123
}
100124
}
101125

102126
// Save album art separately if present
103127
const hasAlbumArt = !!metadata.albumArt;
104128
if (hasAlbumArt && metadata.albumArt) {
105-
await musicIndexedDbHelper.saveAlbumArt(songId, metadata.albumArt);
129+
await musicIndexedDbHelper.saveAlbumArt(
130+
songId,
131+
metadata.albumArt
132+
);
106133
setAlbumArtInCache(songId, metadata.albumArt);
107134
}
108135

109136
const song: Song = {
110137
id: songId,
111138
title: metadata.title,
112139
artist: metadata.artist,
113-
album: metadata.album || t("songInfo.album", { title: t("common.unknownAlbum") }),
140+
album:
141+
metadata.album ||
142+
t("songInfo.album", { title: t("common.unknownAlbum") }),
114143
duration: metadata.duration,
115144
url: "", // Will be set by addSong
116145
albumArt: metadata.albumArt, // Keep for immediate display
@@ -142,8 +171,14 @@ export async function importAudioFiles(
142171
}
143172

144173
toast.dismiss();
174+
// Show accurate results with skipped files count
145175
if (successCount > 0) {
146-
toast.success(t("filePicker.successImport", { count: successCount }));
176+
toast.success(
177+
t("filePicker.successImport", {
178+
count: successCount,
179+
skipped: skippedCount,
180+
})
181+
);
147182
}
148183
if (errorCount > 0) {
149184
toast.error(t("filePicker.failedImport", { count: errorCount }));

Build/src/workers/sw.ts

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,47 @@ precacheAndRoute(self.__WB_MANIFEST);
77
// Handle SPA navigation requests
88
const handler = createHandlerBoundToURL("index.html");
99
const navigationRoute = new NavigationRoute(handler, {
10-
allowlist: [/^\/beta\/HTMLPlayer\//],
10+
allowlist: [/^\/beta\/HTMLPlayer\//],
1111
});
1212
registerRoute(navigationRoute);
1313

1414
// Handle POST requests for file sharing
1515
registerRoute(
16-
({ url, request }) => {
17-
return url.pathname === "/beta/HTMLPlayer/" && request.method === "POST";
18-
},
19-
async ({ event }) => {
20-
try {
21-
const formData = await event.request.formData();
22-
const cache = await caches.open("incoming-shares");
16+
({ url, request }) => {
17+
return url.pathname === "/beta/HTMLPlayer/" && request.method === "POST";
18+
},
19+
async ({ event }) => {
20+
try {
21+
const formData = await event.request.formData();
22+
const cache = await caches.open("incoming-shares");
2323

24-
// Process all files in the form data
25-
const files = [];
26-
for (const [key, value] of formData.entries()) {
27-
if (value instanceof File) {
28-
files.push({ file: value, key });
29-
await cache.put(`/shared-file-${value.name}`, new Response(value, {
30-
headers: { "x-file-name": value.name, "content-type": value.type },
31-
}));
32-
}
33-
}
24+
// Keep track of already cached files during this session
25+
const existingKeys = new Set(
26+
(await cache.keys()).map((key) => key.url.split("/").pop())
27+
);
28+
29+
const files = [];
30+
for (const [value] of formData.entries()) {
31+
if (value instanceof File) {
32+
const uniqueKey = `shared-file-${value.name}`;
33+
if (!existingKeys.has(uniqueKey)) {
34+
// Only cache if it's not already cached
35+
await cache.put(uniqueKey, new Response(value, {
36+
headers: { "x-file-name": value.name },
37+
}));
38+
files.push(uniqueKey);
39+
}
40+
}
41+
}
3442

35-
// Redirect to the app with an indicator for file sharing
36-
const redirectUrl = new URL(
37-
`/beta/HTMLPlayer/?share-received=true&files=${files.length}`,
38-
self.location.origin
39-
);
40-
return Response.redirect(redirectUrl.href, 303);
41-
} catch (e) {
42-
// Always return a response even on error
43-
return new Response("Failed to process file share", { status: 400 });
44-
}
45-
},
46-
"POST"
43+
const redirectUrl = new URL(
44+
`/beta/HTMLPlayer/?share-received=true&files=${files.length}`,
45+
self.location.origin
46+
);
47+
return Response.redirect(redirectUrl.href, 303);
48+
} catch (e) {
49+
return new Response("Failed to process file share", { status: 400 });
50+
}
51+
},
52+
"POST"
4753
);

0 commit comments

Comments
 (0)