Files
2026-08-10 18:58:26 +02:00

224 lines
7.0 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 = {}
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()