mirror of
https://github.com/ergosteur/instaarchive-viewer.git
synced 2026-07-04 11:07:15 -04:00
feat: enable persistent local archives and smart profile fallback
Key changes: - Enabled full metadata caching for local folder archives, allowing them to load instantly from IndexedDB without re-uploading. - Implemented oldest-image fallback for profiles missing an explicit profile picture. - Restored folder-name-to-username detection for local archive uploads. - Optimized scan indexing to track all image files for fallback use.
This commit is contained in:
32
src/App.tsx
32
src/App.tsx
@@ -192,6 +192,37 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}, [handleFiles, setAllPosts, setAllStories, setProfileMetadata, setIsScanning, setScanningPhase]);
|
}, [handleFiles, setAllPosts, setAllStories, setProfileMetadata, setIsScanning, setScanningPhase]);
|
||||||
|
|
||||||
|
const loadLocalCachedArchive = useCallback(async (archive: any) => {
|
||||||
|
console.log(`[Cache] Loading local archive from cache: ${archive.name}`);
|
||||||
|
setIsScanning(true);
|
||||||
|
setCurrentArchive(null);
|
||||||
|
setScanningPhase('Checking Cache');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Small delay for UI transition
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
|
|
||||||
|
setAllPosts(archive.posts || []);
|
||||||
|
setAllStories(archive.stories || []);
|
||||||
|
|
||||||
|
const profileMetadata = { ...archive.profileMetadata };
|
||||||
|
if (!profileMetadata.allProfilePics && archive.allProfilePics) {
|
||||||
|
profileMetadata.allProfilePics = archive.allProfilePics;
|
||||||
|
}
|
||||||
|
if (!profileMetadata.allProfilePics) {
|
||||||
|
profileMetadata.allProfilePics = profileMetadata.profilePic ? [profileMetadata.profilePic] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
setProfileMetadata(profileMetadata);
|
||||||
|
setVisiblePostsCount(90);
|
||||||
|
setIsScanning(false);
|
||||||
|
console.log(`[Cache] Local archive ${archive.name} restored from cache.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Cache] Failed to restore local archive:', err);
|
||||||
|
setIsScanning(false);
|
||||||
|
}
|
||||||
|
}, [setAllPosts, setAllStories, setProfileMetadata, setIsScanning, setScanningPhase]);
|
||||||
|
|
||||||
const handleLocalFiles = (files: FileList | null) => { if (!files) return; const archiveFiles = Array.from(files).map(f => new LocalArchiveFile(f)); handleFiles(archiveFiles); };
|
const handleLocalFiles = (files: FileList | null) => { if (!files) return; const archiveFiles = Array.from(files).map(f => new LocalArchiveFile(f)); handleFiles(archiveFiles); };
|
||||||
const triggerFileSelect = () => fileInputRef.current?.click();
|
const triggerFileSelect = () => fileInputRef.current?.click();
|
||||||
const loadMore = () => setVisiblePostsCount(prev => prev + 90);
|
const loadMore = () => setVisiblePostsCount(prev => prev + 90);
|
||||||
@@ -279,6 +310,7 @@ export default function App() {
|
|||||||
cachedArchives={cachedArchives}
|
cachedArchives={cachedArchives}
|
||||||
onSelect={loadServerArchive}
|
onSelect={loadServerArchive}
|
||||||
onLocalSelect={triggerFileSelect}
|
onLocalSelect={triggerFileSelect}
|
||||||
|
onLocalCacheSelect={loadLocalCachedArchive}
|
||||||
onClearCache={clearCache}
|
onClearCache={clearCache}
|
||||||
isScanning={isScanning}
|
isScanning={isScanning}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ArchiveDashboardProps {
|
|||||||
cachedArchives: Set<string>;
|
cachedArchives: Set<string>;
|
||||||
onSelect: (archive: ServerArchive) => void;
|
onSelect: (archive: ServerArchive) => void;
|
||||||
onLocalSelect: () => void;
|
onLocalSelect: () => void;
|
||||||
|
onLocalCacheSelect: (archive: any) => void;
|
||||||
onClearCache: (name: string) => void;
|
onClearCache: (name: string) => void;
|
||||||
isScanning: boolean;
|
isScanning: boolean;
|
||||||
}
|
}
|
||||||
@@ -24,6 +25,7 @@ export const ArchiveDashboard: React.FC<ArchiveDashboardProps> = ({
|
|||||||
cachedArchives,
|
cachedArchives,
|
||||||
onSelect,
|
onSelect,
|
||||||
onLocalSelect,
|
onLocalSelect,
|
||||||
|
onLocalCacheSelect,
|
||||||
onClearCache,
|
onClearCache,
|
||||||
isScanning
|
isScanning
|
||||||
}) => {
|
}) => {
|
||||||
@@ -107,7 +109,7 @@ export const ArchiveDashboard: React.FC<ArchiveDashboardProps> = ({
|
|||||||
{localArchives.map((archive) => (
|
{localArchives.map((archive) => (
|
||||||
<div key={archive.name} className="relative group text-black">
|
<div key={archive.name} className="relative group text-black">
|
||||||
<button
|
<button
|
||||||
onClick={onLocalSelect}
|
onClick={() => onLocalCacheSelect(archive)}
|
||||||
disabled={isScanning}
|
disabled={isScanning}
|
||||||
className="w-full aspect-[3/4] rounded-xl overflow-hidden bg-white shadow-sm border border-gray-100 hover:shadow-xl hover:scale-[1.02] transition-all flex flex-col text-left disabled:opacity-50"
|
className="w-full aspect-[3/4] rounded-xl overflow-hidden bg-white shadow-sm border border-gray-100 hover:shadow-xl hover:scale-[1.02] transition-all flex flex-col text-left disabled:opacity-50"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export const useArchiveScanner = (
|
|||||||
const postsMap = new Map<string, Partial<Post>>();
|
const postsMap = new Map<string, Partial<Post>>();
|
||||||
const mediaFilesMap = new Map<string, ArchiveFile>();
|
const mediaFilesMap = new Map<string, ArchiveFile>();
|
||||||
const discoveredProfilePics: { name: string, url: string }[] = [];
|
const discoveredProfilePics: { name: string, url: string }[] = [];
|
||||||
|
const allImageFiles: ArchiveFile[] = [];
|
||||||
|
|
||||||
let localFullName = '';
|
let localFullName = '';
|
||||||
let localBio = '';
|
let localBio = '';
|
||||||
@@ -107,7 +108,17 @@ export const useArchiveScanner = (
|
|||||||
return (obj.is_story === true || obj.is_reel_media === true || typeName.includes('Story') || obj.audience === "MediaAudience.DEFAULT" || obj.node_type === "StoryItem" || obj.product_type === "story" || typeName === "GraphStoryVideo" || typeName === "GraphStoryImage");
|
return (obj.is_story === true || obj.is_reel_media === true || typeName.includes('Story') || obj.audience === "MediaAudience.DEFAULT" || obj.node_type === "StoryItem" || obj.product_type === "story" || typeName === "GraphStoryVideo" || typeName === "GraphStoryImage");
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentUsername = archiveContext?.name || currentArchive?.name || detectedUsername || '';
|
let currentUsername = archiveContext?.name || currentArchive?.name || detectedUsername;
|
||||||
|
|
||||||
|
// If still no username (likely local folder), try to extract from path
|
||||||
|
if (!currentUsername && files[0]?.webkitRelativePath) {
|
||||||
|
const pathParts = files[0].webkitRelativePath.split(/[/\\]/);
|
||||||
|
if (pathParts.length > 1) {
|
||||||
|
currentUsername = pathParts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentUsername) currentUsername = 'archived_user';
|
||||||
|
|
||||||
let format: 'export' | 'instaloader' | 'json' | 'unknown' = 'unknown';
|
let format: 'export' | 'instaloader' | 'json' | 'unknown' = 'unknown';
|
||||||
let jsonFiles: ArchiveFile[] = [];
|
let jsonFiles: ArchiveFile[] = [];
|
||||||
@@ -145,6 +156,7 @@ export const useArchiveScanner = (
|
|||||||
|
|
||||||
if (isMedia(file.name)) {
|
if (isMedia(file.name)) {
|
||||||
mediaFilesMap.set(file.webkitRelativePath || file.name, file);
|
mediaFilesMap.set(file.webkitRelativePath || file.name, file);
|
||||||
|
if (isImage(file.name)) allImageFiles.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +322,15 @@ export const useArchiveScanner = (
|
|||||||
const urls = discoveredProfilePics.map(p => p.url);
|
const urls = discoveredProfilePics.map(p => p.url);
|
||||||
localProfilePic = urls[0];
|
localProfilePic = urls[0];
|
||||||
setProfileMetadata(prev => ({ ...prev, profilePic: localProfilePic, allProfilePics: urls }));
|
setProfileMetadata(prev => ({ ...prev, profilePic: localProfilePic, allProfilePics: urls }));
|
||||||
|
} else if (allImageFiles.length > 0) {
|
||||||
|
// Fallback: Use oldest image in archive as profile pic
|
||||||
|
allImageFiles.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
const oldestFile = allImageFiles[0];
|
||||||
|
try {
|
||||||
|
const url = oldestFile.url || URL.createObjectURL(new Blob([await oldestFile.arrayBuffer()], { type: 'image/jpeg' }));
|
||||||
|
localProfilePic = url;
|
||||||
|
setProfileMetadata(prev => ({ ...prev, profilePic: localProfilePic, allProfilePics: [url] }));
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalUsername = currentUsername || 'archived_user';
|
const finalUsername = currentUsername || 'archived_user';
|
||||||
@@ -358,7 +379,8 @@ export const useArchiveScanner = (
|
|||||||
|
|
||||||
const cacheData = {
|
const cacheData = {
|
||||||
name: cacheKey, isLocal, fileCount: archiveToCache ? archiveToCache.fileCount : files.length,
|
name: cacheKey, isLocal, fileCount: archiveToCache ? archiveToCache.fileCount : files.length,
|
||||||
posts: isLocal ? [] : posts, stories: isLocal ? [] : stories,
|
posts: posts, // Enable caching for local archives
|
||||||
|
stories: stories,
|
||||||
profileMetadata: {
|
profileMetadata: {
|
||||||
username: finalUsername,
|
username: finalUsername,
|
||||||
fullName: localFullName,
|
fullName: localFullName,
|
||||||
|
|||||||
Reference in New Issue
Block a user