mirror of
https://github.com/mudabbir-ahmad/Media-Aggregation-and-Sorting-System.git
synced 2026-10-07 19:40:20 +00:00
feat: update NAS configuration and enhance UI components for better user experience
This commit is contained in:
1 parent
8e6fa5270b
commit
6167418074
23 files changed
+2147
-702
No files matched your search
@@ -0,0 +1,320 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const config = require('./config');
|
||||
|
||||
let db = null;
|
||||
|
||||
function getDb() {
|
||||
if (db) return db;
|
||||
config.ensureConfigDir();
|
||||
db = new Database(config.DB_PATH);
|
||||
db.pragma('journal_mode = WAL');
|
||||
initSchema();
|
||||
return db;
|
||||
}
|
||||
|
||||
function initSchema() {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS photos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
filename TEXT NOT NULL,
|
||||
filepath TEXT NOT NULL,
|
||||
sha256_hash TEXT NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
mime_type TEXT NOT NULL,
|
||||
indexed_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
UNIQUE(user_id, sha256_hash)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS albums (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
UNIQUE(user_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS album_photos (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
album_id INTEGER NOT NULL,
|
||||
photo_id INTEGER NOT NULL,
|
||||
added_at TEXT DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (album_id) REFERENCES albums(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (photo_id) REFERENCES photos(id) ON DELETE CASCADE,
|
||||
UNIQUE(album_id, photo_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_photos_user ON photos(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_photos_hash ON photos(sha256_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_albums_user ON albums(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_album_photos_album ON album_photos(album_id);
|
||||
`);
|
||||
}
|
||||
|
||||
function ensureUser(username) {
|
||||
const d = getDb();
|
||||
const existing = d.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||||
if (existing) return existing.id;
|
||||
const result = d.prepare('INSERT INTO users (username) VALUES (?)').run(username);
|
||||
return Number(result.lastInsertRowid);
|
||||
}
|
||||
|
||||
function hashFile(filePath) {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
return crypto.createHash('sha256').update(buffer).digest('hex');
|
||||
}
|
||||
|
||||
function getMimeType(ext) {
|
||||
const map = {
|
||||
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
|
||||
'.gif': 'image/gif', '.bmp': 'image/bmp', '.webp': 'image/webp',
|
||||
'.heic': 'image/heic', '.heif': 'image/heif',
|
||||
'.tiff': 'image/tiff', '.tif': 'image/tiff', '.svg': 'image/svg+xml',
|
||||
};
|
||||
return map[ext.toLowerCase()] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
const SUPPORTED_EXTS = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.heic', '.heif', '.tiff', '.tif', '.svg'];
|
||||
|
||||
function indexPhotosForUser(username, photosBasePath) {
|
||||
const d = getDb();
|
||||
const userId = ensureUser(username);
|
||||
|
||||
const userDir = path.join(photosBasePath, username);
|
||||
if (!fs.existsSync(userDir)) {
|
||||
fs.mkdirSync(userDir, { recursive: true });
|
||||
return { indexed: 0, skipped: 0, errors: 0, total: 0 };
|
||||
}
|
||||
|
||||
const files = getAllMediaFiles(userDir);
|
||||
|
||||
let indexed = 0, skipped = 0, errors = 0;
|
||||
|
||||
const insertStmt = d.prepare(`
|
||||
INSERT OR IGNORE INTO photos (user_id, filename, filepath, sha256_hash, file_size, mime_type)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
for (const fullPath of files) {
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
const hash = hashFile(fullPath);
|
||||
const ext = path.extname(fullPath);
|
||||
const mime = getMimeType(ext);
|
||||
const filename = path.basename(fullPath);
|
||||
|
||||
const result = insertStmt.run(userId, filename, fullPath, hash, stat.size, mime);
|
||||
if (result.changes > 0) {
|
||||
indexed++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
} catch (err) {
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
return { indexed, skipped, errors, total: files.length };
|
||||
}
|
||||
|
||||
// Recursively find all media files in a directory
|
||||
function getAllMediaFiles(dir) {
|
||||
let results = [];
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results = results.concat(getAllMediaFiles(fullPath));
|
||||
} else {
|
||||
const ext = path.extname(entry.name).toLowerCase();
|
||||
if (SUPPORTED_EXTS.includes(ext)) {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function getPhotoIndex(username) {
|
||||
const d = getDb();
|
||||
const user = d.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||||
if (!user) return [];
|
||||
return d.prepare(`
|
||||
SELECT id, filename, sha256_hash, file_size, mime_type, indexed_at
|
||||
FROM photos WHERE user_id = ? ORDER BY indexed_at DESC
|
||||
`).all(user.id);
|
||||
}
|
||||
|
||||
function getPhotoById(photoId) {
|
||||
const d = getDb();
|
||||
return d.prepare(`
|
||||
SELECT p.*, u.username FROM photos p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.id = ?
|
||||
`).get(photoId);
|
||||
}
|
||||
|
||||
function getPhotoByHash(hash) {
|
||||
const d = getDb();
|
||||
return d.prepare(`
|
||||
SELECT p.*, u.username FROM photos p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.sha256_hash = ?
|
||||
`).get(hash);
|
||||
}
|
||||
|
||||
function getAllUsers() {
|
||||
const d = getDb();
|
||||
return d.prepare('SELECT * FROM users ORDER BY username').all();
|
||||
}
|
||||
|
||||
function removeOrphanedEntries(username, photosBasePath) {
|
||||
const d = getDb();
|
||||
const user = d.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||||
if (!user) return 0;
|
||||
|
||||
const photos = d.prepare('SELECT id, filepath FROM photos WHERE user_id = ?').all(user.id);
|
||||
let removed = 0;
|
||||
const deleteStmt = d.prepare('DELETE FROM photos WHERE id = ?');
|
||||
|
||||
for (const photo of photos) {
|
||||
if (!fs.existsSync(photo.filepath)) {
|
||||
deleteStmt.run(photo.id);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
function deletePhoto(photoId) {
|
||||
const d = getDb();
|
||||
const photo = getPhotoById(photoId);
|
||||
if (!photo) return { success: false, error: 'Photo not found' };
|
||||
|
||||
// Remove from disk
|
||||
if (fs.existsSync(photo.filepath)) {
|
||||
fs.unlinkSync(photo.filepath);
|
||||
}
|
||||
|
||||
// Remove from album_photos first (cascade might not work with all builds)
|
||||
d.prepare('DELETE FROM album_photos WHERE photo_id = ?').run(photoId);
|
||||
d.prepare('DELETE FROM photos WHERE id = ?').run(photoId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// ─── Album operations ───
|
||||
|
||||
function createAlbum(username, albumName) {
|
||||
const d = getDb();
|
||||
const userId = ensureUser(username);
|
||||
try {
|
||||
const result = d.prepare('INSERT INTO albums (user_id, name) VALUES (?, ?)').run(userId, albumName);
|
||||
return { success: true, albumId: Number(result.lastInsertRowid) };
|
||||
} catch (err) {
|
||||
if (err.message.includes('UNIQUE')) {
|
||||
return { success: false, error: 'Album already exists' };
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function getAlbums(username) {
|
||||
const d = getDb();
|
||||
const user = d.prepare('SELECT id FROM users WHERE username = ?').get(username);
|
||||
if (!user) return [];
|
||||
|
||||
const albums = d.prepare('SELECT * FROM albums WHERE user_id = ? ORDER BY created_at DESC').all(user.id);
|
||||
return albums.map(album => {
|
||||
const count = d.prepare('SELECT COUNT(*) as cnt FROM album_photos WHERE album_id = ?').get(album.id);
|
||||
const cover = d.prepare(`
|
||||
SELECT p.id, p.filename, p.sha256_hash FROM album_photos ap
|
||||
JOIN photos p ON ap.photo_id = p.id
|
||||
WHERE ap.album_id = ? ORDER BY ap.added_at DESC LIMIT 1
|
||||
`).get(album.id);
|
||||
return { ...album, photoCount: count.cnt, coverPhoto: cover || null };
|
||||
});
|
||||
}
|
||||
|
||||
function getAlbumPhotos(albumId) {
|
||||
const d = getDb();
|
||||
return d.prepare(`
|
||||
SELECT p.id, p.filename, p.sha256_hash, p.file_size, p.mime_type, p.indexed_at
|
||||
FROM album_photos ap
|
||||
JOIN photos p ON ap.photo_id = p.id
|
||||
WHERE ap.album_id = ?
|
||||
ORDER BY ap.added_at DESC
|
||||
`).all(albumId);
|
||||
}
|
||||
|
||||
function addPhotoToAlbum(albumId, photoId) {
|
||||
const d = getDb();
|
||||
try {
|
||||
d.prepare('INSERT OR IGNORE INTO album_photos (album_id, photo_id) VALUES (?, ?)').run(albumId, photoId);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
function removePhotoFromAlbum(albumId, photoId) {
|
||||
const d = getDb();
|
||||
d.prepare('DELETE FROM album_photos WHERE album_id = ? AND photo_id = ?').run(albumId, photoId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
function deleteAlbum(albumId) {
|
||||
const d = getDb();
|
||||
d.prepare('DELETE FROM album_photos WHERE album_id = ?').run(albumId);
|
||||
d.prepare('DELETE FROM albums WHERE id = ?').run(albumId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
function renameAlbum(albumId, newName) {
|
||||
const d = getDb();
|
||||
try {
|
||||
d.prepare('UPDATE albums SET name = ? WHERE id = ?').run(newName, albumId);
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
function closeDb() {
|
||||
if (db) {
|
||||
db.close();
|
||||
db = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getDb,
|
||||
ensureUser,
|
||||
hashFile,
|
||||
indexPhotosForUser,
|
||||
getPhotoIndex,
|
||||
getPhotoById,
|
||||
getPhotoByHash,
|
||||
getAllUsers,
|
||||
removeOrphanedEntries,
|
||||
deletePhoto,
|
||||
createAlbum,
|
||||
getAlbums,
|
||||
getAlbumPhotos,
|
||||
addPhotoToAlbum,
|
||||
removePhotoFromAlbum,
|
||||
deleteAlbum,
|
||||
renameAlbum,
|
||||
closeDb,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user