Add upload.js: a CommonJS Node script to bulk upload files to Immich with optional image compression and album support. Also add package.json and package-lock.json listing dependencies (@immich/sdk, sharp, dotenv, mime-types, colors). The script uses env vars (API_KEY, BASE_URL, UPLOAD_DIR, COMPRESSION, DO_ALBUMS, etc.) to control behavior.
196 lines
6.2 KiB
JavaScript
196 lines
6.2 KiB
JavaScript
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 = {}
|
|
|
|
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.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) {
|
|
try {
|
|
console.log(`Compressing file: ${file.yellow.bold}`)
|
|
const compressedFile = await sharp(file)
|
|
.toFormat(process.env.COMPRESSION_OUTPUT_FORMAT,
|
|
{ quality: parseInt(process.env.COMPRESSION_OUTPUT_QUALITY) || 80 }
|
|
)
|
|
.toFile(changeExtension(file, process.env.COMPRESSION_OUTPUT_FORMAT))
|
|
await fs.promises.rm(file)
|
|
} catch (error) {
|
|
console.log(`Error occured while compressing file: ${file.red.bold}`)
|
|
console.log(error)
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
main()
|