Add Immich bulk upload script and package files
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.
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user