import { getDatabase } from './database' import { Photo, Album, Tag, Directory, ImageHash, PhotoHash } from '@/types/photo' import { randomUUID } from 'crypto' export class PhotoService { private db = getDatabase() // Photo operations createPhoto(photoData: Omit): Photo { const id = randomUUID() const insertPhoto = this.db.prepare(` INSERT INTO photos ( id, filename, filepath, directory, filesize, created_at, modified_at, width, height, format, favorite, rating, description, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) try { // Sanitize each value to ensure it's a primitive type for SQLite const sanitizeValue = (value: any): string | number | boolean | null => { // Explicitly handle null and undefined if (value === null || value === undefined) return null // Handle primitive types if (typeof value === 'string') return value if (typeof value === 'number') return value if (typeof value === 'boolean') return value // Handle other types if (typeof value === 'bigint') return Number(value) if (value instanceof Date) return value.toISOString() if (typeof value === 'object') { // If it's null, return null (redundant but explicit) if (value === null) return null // Otherwise stringify the object return JSON.stringify(value) } // Convert everything else to string return String(value) } // Direct approach - build parameters with explicit type checking const params = [ id, // string photoData.filename, // string photoData.filepath, // string photoData.directory, // string photoData.filesize, // number photoData.created_at, // string photoData.modified_at, // string photoData.width || null, // number or null photoData.height || null, // number or null photoData.format || null, // string or null photoData.favorite === true ? 1 : 0, // convert boolean to integer null, // rating - always null for now null, // description - always null for now photoData.metadata || null // string or null ] // Debug: Log each parameter type before database insertion console.log(`[PHOTO SERVICE] Debug - checking SQL parameters for ${photoData.filename}:`) const fieldNames = ['id', 'filename', 'filepath', 'directory', 'filesize', 'created_at', 'modified_at', 'width', 'height', 'format', 'favorite', 'rating', 'description', 'metadata'] params.forEach((value, index) => { const valueStr = value === null ? 'NULL' : (typeof value === 'string' && value.length > 50 ? value.substring(0, 50) + '...' : value) console.log(` ${fieldNames[index]}:`, typeof value, valueStr) // Additional check for SQLite compatibility const isValidSQLiteValue = value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint' if (!isValidSQLiteValue) { console.error(` ❌ INVALID SQLite value for ${fieldNames[index]}:`, value, typeof value) } }) // Use spread operator with the clean params array insertPhoto.run(...params) return this.getPhoto(id)! } catch (error) { console.error(`[PHOTO SERVICE] Error creating photo ${photoData.filename}:`, error) console.error(`[PHOTO SERVICE] Photo data:`, photoData) throw error } } getPhoto(id: string): Photo | null { const selectPhoto = this.db.prepare('SELECT * FROM photos WHERE id = ?') return selectPhoto.get(id) as Photo | null } getPhotoByPath(filepath: string): Photo | null { const selectPhoto = this.db.prepare('SELECT * FROM photos WHERE filepath = ?') return selectPhoto.get(filepath) as Photo | null } getPhotos(options: { directory?: string limit?: number offset?: number sortBy?: 'created_at' | 'modified_at' | 'filename' | 'filesize' sortOrder?: 'ASC' | 'DESC' favorite?: boolean rating?: number } = {}): Photo[] { let query = 'SELECT * FROM photos WHERE 1=1' const params: any[] = [] if (options.directory) { query += ' AND directory = ?' params.push(options.directory) } if (options.favorite !== undefined) { query += ' AND favorite = ?' params.push(options.favorite) } if (options.rating !== undefined) { query += ' AND rating = ?' params.push(options.rating) } const sortBy = options.sortBy || 'created_at' const sortOrder = options.sortOrder || 'DESC' query += ` ORDER BY ${sortBy} ${sortOrder}` if (options.limit) { query += ' LIMIT ?' params.push(options.limit) } if (options.offset) { query += ' OFFSET ?' params.push(options.offset) } const selectPhotos = this.db.prepare(query) return selectPhotos.all(...params) as Photo[] } updatePhoto(id: string, updates: Partial>): Photo | null { const fields = Object.keys(updates).filter(key => updates[key as keyof typeof updates] !== undefined) if (fields.length === 0) return this.getPhoto(id) const setClause = fields.map(field => `${field} = ?`).join(', ') const values = fields.map(field => updates[field as keyof typeof updates]) const updatePhoto = this.db.prepare(`UPDATE photos SET ${setClause} WHERE id = ?`) updatePhoto.run(...values, id) return this.getPhoto(id) } deletePhoto(id: string): boolean { const deletePhoto = this.db.prepare('DELETE FROM photos WHERE id = ?') const result = deletePhoto.run(id) return result.changes > 0 } // Album operations createAlbum(albumData: Omit): Album { const id = randomUUID() const insertAlbum = this.db.prepare(` INSERT INTO albums (id, name, description, cover_photo_id) VALUES (?, ?, ?, ?) `) insertAlbum.run(id, albumData.name, albumData.description, albumData.cover_photo_id) return this.getAlbum(id)! } getAlbum(id: string): Album | null { const selectAlbum = this.db.prepare('SELECT * FROM albums WHERE id = ?') return selectAlbum.get(id) as Album | null } getAlbums(): Album[] { const selectAlbums = this.db.prepare('SELECT * FROM albums ORDER BY name') return selectAlbums.all() as Album[] } addPhotoToAlbum(photoId: string, albumId: string): boolean { const insertPhotoAlbum = this.db.prepare(` INSERT OR IGNORE INTO photo_albums (photo_id, album_id) VALUES (?, ?) `) const result = insertPhotoAlbum.run(photoId, albumId) return result.changes > 0 } removePhotoFromAlbum(photoId: string, albumId: string): boolean { const deletePhotoAlbum = this.db.prepare(` DELETE FROM photo_albums WHERE photo_id = ? AND album_id = ? `) const result = deletePhotoAlbum.run(photoId, albumId) return result.changes > 0 } getAlbumPhotos(albumId: string): Photo[] { const selectAlbumPhotos = this.db.prepare(` SELECT p.* FROM photos p JOIN photo_albums pa ON p.id = pa.photo_id WHERE pa.album_id = ? ORDER BY pa.added_at DESC `) return selectAlbumPhotos.all(albumId) as Photo[] } // Tag operations createTag(tagData: Omit): Tag { const id = randomUUID() const insertTag = this.db.prepare(` INSERT INTO tags (id, name, color) VALUES (?, ?, ?) `) insertTag.run(id, tagData.name, tagData.color) return this.getTag(id)! } getTag(id: string): Tag | null { const selectTag = this.db.prepare('SELECT * FROM tags WHERE id = ?') return selectTag.get(id) as Tag | null } getTags(): Tag[] { const selectTags = this.db.prepare('SELECT * FROM tags ORDER BY name') return selectTags.all() as Tag[] } addPhotoTag(photoId: string, tagId: string): boolean { const insertPhotoTag = this.db.prepare(` INSERT OR IGNORE INTO photo_tags (photo_id, tag_id) VALUES (?, ?) `) const result = insertPhotoTag.run(photoId, tagId) return result.changes > 0 } removePhotoTag(photoId: string, tagId: string): boolean { const deletePhotoTag = this.db.prepare(` DELETE FROM photo_tags WHERE photo_id = ? AND tag_id = ? `) const result = deletePhotoTag.run(photoId, tagId) return result.changes > 0 } getPhotoTags(photoId: string): Tag[] { const selectPhotoTags = this.db.prepare(` SELECT t.* FROM tags t JOIN photo_tags pt ON t.id = pt.tag_id WHERE pt.photo_id = ? ORDER BY t.name `) return selectPhotoTags.all(photoId) as Tag[] } // Directory operations createOrUpdateDirectory(directoryData: Omit): Directory { const existingDir = this.getDirectoryByPath(directoryData.path) if (existingDir) { const updateDir = this.db.prepare(` UPDATE directories SET name = ?, last_scanned = ?, photo_count = ?, total_size = ? WHERE path = ? `) updateDir.run( directoryData.name, directoryData.last_scanned, directoryData.photo_count, directoryData.total_size, directoryData.path ) return this.getDirectoryByPath(directoryData.path)! } else { const id = randomUUID() const insertDir = this.db.prepare(` INSERT INTO directories (id, path, name, last_scanned, photo_count, total_size) VALUES (?, ?, ?, ?, ?, ?) `) insertDir.run( id, directoryData.path, directoryData.name, directoryData.last_scanned, directoryData.photo_count, directoryData.total_size ) return this.getDirectory(id)! } } getDirectory(id: string): Directory | null { const selectDir = this.db.prepare('SELECT * FROM directories WHERE id = ?') return selectDir.get(id) as Directory | null } getDirectoryByPath(path: string): Directory | null { const selectDir = this.db.prepare('SELECT * FROM directories WHERE path = ?') return selectDir.get(path) as Directory | null } getDirectories(): Directory[] { const selectDirs = this.db.prepare('SELECT * FROM directories ORDER BY last_scanned DESC') return selectDirs.all() as Directory[] } deleteDirectory(id: string): boolean { const deleteDir = this.db.prepare('DELETE FROM directories WHERE id = ?') const result = deleteDir.run(id) return result.changes > 0 } // Statistics getPhotoCount(): number { const countPhotos = this.db.prepare('SELECT COUNT(*) as count FROM photos') return (countPhotos.get() as { count: number }).count } getTotalFileSize(): number { const totalSize = this.db.prepare('SELECT SUM(filesize) as total FROM photos') return (totalSize.get() as { total: number }).total || 0 } // Hash operations createOrUpdateImageHash(sha256Hash: string): ImageHash { const existingHash = this.getImageHashBySha256(sha256Hash) if (existingHash) { // Update file count const updateHash = this.db.prepare(` UPDATE image_hashes SET file_count = file_count + 1 WHERE sha256_hash = ? `) updateHash.run(sha256Hash) return this.getImageHashBySha256(sha256Hash)! } else { // Create new hash record const id = randomUUID() const insertHash = this.db.prepare(` INSERT INTO image_hashes (id, sha256_hash, file_count) VALUES (?, ?, 1) `) insertHash.run(id, sha256Hash) return this.getImageHash(id)! } } getImageHash(id: string): ImageHash | null { const selectHash = this.db.prepare('SELECT * FROM image_hashes WHERE id = ?') return selectHash.get(id) as ImageHash | null } getImageHashBySha256(sha256Hash: string): ImageHash | null { const selectHash = this.db.prepare('SELECT * FROM image_hashes WHERE sha256_hash = ?') return selectHash.get(sha256Hash) as ImageHash | null } associatePhotoWithHash(photoId: string, hashId: string): boolean { const insertPhotoHash = this.db.prepare(` INSERT OR IGNORE INTO photo_hashes (photo_id, hash_id) VALUES (?, ?) `) const result = insertPhotoHash.run(photoId, hashId) return result.changes > 0 } getPhotosByHash(sha256Hash: string): Photo[] { const selectPhotos = this.db.prepare(` SELECT p.* FROM photos p JOIN photo_hashes ph ON p.id = ph.photo_id JOIN image_hashes ih ON ph.hash_id = ih.id WHERE ih.sha256_hash = ? ORDER BY p.created_at DESC `) return selectPhotos.all(sha256Hash) as Photo[] } getDuplicateHashes(): ImageHash[] { const selectDuplicates = this.db.prepare(` SELECT * FROM image_hashes WHERE file_count > 1 ORDER BY file_count DESC `) return selectDuplicates.all() as ImageHash[] } getPhotosWithDuplicates(): Photo[] { const selectPhotos = this.db.prepare(` SELECT DISTINCT p.* FROM photos p JOIN photo_hashes ph ON p.id = ph.photo_id JOIN image_hashes ih ON ph.hash_id = ih.id WHERE ih.file_count > 1 ORDER BY p.created_at DESC `) return selectPhotos.all() as Photo[] } // Thumbnail caching operations getCachedThumbnail(photoId: string, size: number): Buffer | null { const selectThumbnail = this.db.prepare(` SELECT thumbnail_blob FROM photos WHERE id = ? AND thumbnail_size = ? AND thumbnail_blob IS NOT NULL `) const result = selectThumbnail.get(photoId, size) as { thumbnail_blob: Buffer } | null return result?.thumbnail_blob || null } updateThumbnailCache(photoId: string, size: number, thumbnailData: Buffer): boolean { const updateThumbnail = this.db.prepare(` UPDATE photos SET thumbnail_blob = ?, thumbnail_size = ?, thumbnail_generated_at = CURRENT_TIMESTAMP WHERE id = ? `) const result = updateThumbnail.run(thumbnailData, size, photoId) return result.changes > 0 } clearThumbnailCache(photoId?: string): boolean { if (photoId) { const clearThumbnail = this.db.prepare(` UPDATE photos SET thumbnail_blob = NULL, thumbnail_size = NULL, thumbnail_generated_at = NULL WHERE id = ? `) const result = clearThumbnail.run(photoId) return result.changes > 0 } else { // Clear all thumbnail caches const clearAllThumbnails = this.db.prepare(` UPDATE photos SET thumbnail_blob = NULL, thumbnail_size = NULL, thumbnail_generated_at = NULL `) const result = clearAllThumbnails.run() return result.changes > 0 } } } export const photoService = new PhotoService()