require("colors") require("dotenv").config() const fs = require("node:fs") const path = require("node:path") const mime = require("mime-types") const crypto = require("node:crypto") const immich = require("@immich/sdk") const sharp = require("sharp") const albumMap = new Map() const assetsList = {} const IMAGE_EXTENSIONS = new Set([ ".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".gif", ".tiff", ".bmp", ".svg", ".avif", ".raw", ".dng" ]) const VIDEO_EXTENSIONS = new Set([ ".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv", ".m4v", ".3gp", ".ts" ]) async function main() { immich.init({ apiKey: process.env.API_KEY, baseUrl: process.env.BASE_URL }) const user = await immich.getMyUser() console.log(`Logged as: ${user.name.cyan.bold}`) if (process.env.IMG_COMPRESSION == "true") { console.log("") console.log("Processing files compression...") await processFilesCompression() console.log("") } const folders = await getFoldersWithFiles(process.env.UPLOAD_DIR) if (process.env.DO_ALBUMS == "true") { console.log("") console.log(`Creating albums...`) await processAlbums() console.log("") } for (const folder of folders) { await processFolder(folder) } if (process.env.DO_ALBUMS == "true") { console.log("") console.log(`Attaching assets to albums...`) await processAssetsAlbums() } } async function processFilesCompression() { const files = await getFilesRecursive(process.env.UPLOAD_DIR) for (const file of files) { switch (getMediaType(file)) { case "image": try { console.log(`Compressing image: ${file.yellow.bold}`) const compressedFile = await sharp(file) .toFormat(process.env.IMG_COMPRESSION_OUTPUT_FORMAT, { quality: parseInt(process.env.IMG_COMPRESSION_OUTPUT_QUALITY) || 80 } ) .toFile(changeExtension(file, process.env.IMG_COMPRESSION_OUTPUT_FORMAT)) await fs.promises.rm(file) } catch (error) { console.log(`Error occured while compressing image: ${file.red.bold}`) console.log(error) } break default: break } } } async function processAlbums() { const albums = await getFoldersWithFiles(process.env.UPLOAD_DIR) for (const album of albums) { const albumName = stripParentDirectory(album) const immichAlbum = await immich.getAllAlbums({ name: albumName }) if (immichAlbum.length == 0) { const createdAlbum = await immich.createAlbum({ createAlbumDto: { albumName } }) albumMap.set(album, createdAlbum.id) } else { albumMap.set(album, immichAlbum[0].id) } console.log(` Album processed: ${albumName.magenta.bold} (${albumMap.get(album).grey.bold})`) } } async function processAssetsAlbums() { for (const albumId in assetsList) { if (!Object.hasOwn(assetsList, albumId)) continue const assets = assetsList[albumId] await immich.addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assets } }) console.log(` Albums assets processed: ${albumId.magenta.bold}`) } } async function processFolder(folder) { console.log(`Processing folder: ${folder.magenta.bold}`) const entries = fs.readdirSync(path.join(process.cwd(), folder), { withFileTypes: true }) for (const entry of entries) { if (!entry.isFile()) { continue } try { const result = await processFile(path.posix.join(folder, entry.name)) if (process.env.DO_ALBUMS == "true") { const album = path.dirname(result.filePath) if (!Array.isArray(assetsList[albumMap.get(album)])) { assetsList[albumMap.get(album)] = [] } assetsList[albumMap.get(album)].push(result.fileId) } console.log(` File processed: ${result.fileName.yellow.bold}`) console.log(` └─Status: ${result.status.gray.bold}`) } catch (error) { console.log(`Error occured while processing folder: ${folder.red.bold}`) console.log(error) } } console.log("") } async function processFile(filePath) { try { const stats = await fs.promises.stat(filePath) const fileCreatedAt = stats.birthtime.toISOString() const fileModifiedAt = stats.mtime.toISOString() const mimeType = mime.lookup(filePath) || "application/octet-stream" const fileBlob = await fs.openAsBlob(filePath) const fileName = path.basename(filePath) const fileData = new File([fileBlob], fileName, { type: mimeType }) const uploadResult = await immich.uploadAsset({ assetMediaCreateDto: { assetData: fileData, fileCreatedAt: fileCreatedAt, fileModifiedAt: fileModifiedAt } }) return { fileName, filePath, fileId: uploadResult.id, status: uploadResult.status } } catch (error) { throw error } } async function getFoldersWithFiles(dirPath) { let foldersWithFiles = [] const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }) const hasFile = entries.some(entry => entry.isFile()) if (hasFile) { foldersWithFiles.push(dirPath) } for (const entry of entries) { if (entry.isDirectory()) { const fullSubPath = path.posix.join(dirPath, entry.name) const subFolderResults = await getFoldersWithFiles(fullSubPath) foldersWithFiles = foldersWithFiles.concat(subFolderResults) } } return foldersWithFiles } function stripParentDirectory(myPath) { const splitedPath = myPath.split(path.posix.sep) splitedPath.splice(0, 1) const stripedPath = splitedPath.join(" ") return stripedPath } async function getFilesRecursive(dirPath) { let results = [] const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(dirPath, entry.name) if (entry.isDirectory()) { const subFiles = await getFilesRecursive(fullPath) results = results.concat(subFiles) } else if (entry.isFile()) { results.push(fullPath) } } return results } function changeExtension(filePath, newExt) { const formattedExt = newExt.startsWith(".") ? newExt : `.${newExt}` const parsed = path.parse(filePath) parsed.base = `${parsed.name}${formattedExt}` parsed.ext = formattedExt return path.format(parsed) } function getMediaType(filePath) { if (!filePath) return "unknown" const ext = path.extname(filePath).toLowerCase() if (IMAGE_EXTENSIONS.has(ext)) return "image" if (VIDEO_EXTENSIONS.has(ext)) return "video" return "unknown" } main()