Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save davidacampos/47138edcdc3ac040c05e8f3d6f6a9224 to your computer and use it in GitHub Desktop.

Select an option

Save davidacampos/47138edcdc3ac040c05e8f3d6f6a9224 to your computer and use it in GitHub Desktop.
/**
DISCLAIMER: DO NOT COPY & PASTE RANDOM JAVASCRIPT SCRIPTS INTO YOUR BROWSER'S CONSOLE WITHOUT UNDERSTANDING WHAT THEY ARE DOING. THEY CAN STEAL YOUR PERSONAL INFORMATION!!!
What this script does?
Based on an external library in immich, it goes through all the folder of it, and creates albums for each one
How it works?
1- Gets and deletes any existing albums, whose description match the one used when generating them (if any... this is for the only purpose of "regenerating" the folders if the script is run more than once)
2- Gets all existing assets (i.e. photos and videos). This might take a while, in my case the generated JSON was over 100MB.
3- Goes through each one of them, and if they have the originalPath value (I assume that's only for external? I don't have non-external assets) extract the top folder from it and add it to our list of albums to create.
4- With the list of top folders, create albums and include the assets as needed.
How to run?
1- Configure the PARAMETERS section as needed (the critical options are rootPath and albumGenerationLimit).
2- In a web browser, open (and log in) your Immich instance.
3- Open the browser's console (pressing F12 in Chrome in Windows) and copy & paste, then press enter.
4- Progress will be shown in the console's output
*/
(async function () {
//** PARAMETERS */
/***********************************************************/
let immichHost = ""; // This could be left blank if running the script within the same immich instance or something like https://IMMICH_HOST
let rootPath = "/mnt/pictures/"; // This will be used as the "root" folder... meaning albums will be created based on folders directly under "root" (e.g. /mnt/pictures/ChristmasHoliday will end up as album "ChristmasHoliday")
let albumDescription = "Auto-generated from external library"; // This is needed to use as a key when/if running the script multiple times
let foldersToExclude = []; // Optional, this is to exclude folders from been created as albums
let albumGenerationLimit = 5; // Max number of albums auto-generated. Need to change to something like 9999 to create all albums
/***********************************************************/
// Get all albums
console.log("Fetching all albums...");
let albums = await (await fetch(immichHost + "/api/albums", {
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "include"
})).json();
// Go through all albums, and delete the ones that have the right "album description"
console.log("Deleting previously auto-generated albums...");
albums.forEach(async (album, index) => {
if (album.description == albumDescription) {
setTimeout(async function () {
await fetch(immichHost + "/api/albums/" + album.id, {
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
},
"method": "DELETE",
"mode": "cors",
"credentials": "include"
});
console.log("Deleted album [" + index + "/" + albums.length + "]");
}, index * 250); // Space out each album deletion by 250ms to not hit all at once
}
});
let assets = [];
let nextPage = 1;
while (nextPage != null) {
// The API call is paginated, so we call in batches
let rtn = await (await fetch(immichHost + "/api/search/metadata", {
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
},
"body": JSON.stringify({
size: 1000,
page: nextPage
}),
"method": "POST",
"mode": "cors",
"credentials": "include"
})).json();
// Add the retrieved assets to the list of all assets
assets.push(...rtn.assets.items);
nextPage = rtn.assets.nextPage;
};
// Go through all assets and create mapping of "folders" and what assets are in each folder
let albumsWithAssets = {};
assets.forEach(asset => {
if (asset.originalPath) {
let folder = asset.originalPath.replace(rootPath, "").split("/")[0];
if (foldersToExclude.includes(folder) == false) {
if (!albumsWithAssets[folder]) {
albumsWithAssets[folder] = {
assets: []
}
}
albumsWithAssets[folder].assets.push(asset.id);
}
}
});
// For all the "folders" identified, create an immich album for each, adding the respective assets to each
console.log("Creating all albums with their assets...");
Object.keys(albumsWithAssets).forEach(async (folder, index) => {
// Only if there is more than 1 file on a given "folder" (i.e. do NOT create albums for single files)
if (albumsWithAssets[folder].assets.length > 1) {
let body = {
albumName: folder,
description: albumDescription,
assetIds: albumsWithAssets[folder].assets
}
if (index < albumGenerationLimit) { // Limits the number of albums auto-generated, mainly for testing purposes
setTimeout(async function () {
await fetch(immichHost + "/api/albums", {
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
},
"body": JSON.stringify(body),
"method": "POST",
"mode": "cors",
"credentials": "include"
});
console.log("Created album [" + index + "/" + Object.keys(albumsWithAssets).length + "] for: " + folder);
}, index * 250); // Space out each album creation by 250ms to not hit all at once
}
}
});
if (albumGenerationLimit < Object.keys(albumsWithAssets).length) {
console.warn("Not all folders will be created. Confirm the albumGenerationLimit parameter was properly specified");
}
})();
@hectorzin
Copy link

There is a bug, you must modify this line
if (asset.originalPath) {
by
if (asset.originalPath && asset.originalPath.includes(rootPath)) {
if not, if you have photos added, or photos in the trash, the internal folder used by immish to store photos that are numbers, are added as albums, you must ensure that the originalPath is inside the external library path.

Also, if you want to modify it to use the last folder nanme in the structrure of folders and not the first one, the change I did is the next one

changed
let folder = asset.originalPath.replace(rootPath, "").split("/")[0];
by
let segments = asset.originalPath.replace(rootPath, "").split("/");
let folder = segments[segments.length - 2];

Thanks for your script.!

@znonymous29
Copy link

GET http://localhost:2283/api/asset 404 (Not Found) It seems like that old asset search doesn't work any more.

immich-app/immich#9104

@davidacampos
Copy link
Author

GET http://localhost:2283/api/asset 404 (Not Found) It seems like that old asset search doesn't work any more.

immich-app/immich#9104

You are right. Just updated the script to use the new API. This works as of v1.108.0

@znonymous29
Copy link

GET http://localhost:2283/api/asset 404 (Not Found) It seems like that old asset search doesn't work any more.

immich-app/immich#9104

You are right. Just updated the script to use the new API. This works as of v1.108.0

Thanks a lot. It works now.

@thorsten-hehn
Copy link

thorsten-hehn commented Oct 3, 2024

There is a bug, you must modify this line if (asset.originalPath) { by if (asset.originalPath && asset.originalPath.includes(rootPath)) { if not, if you have photos added, or photos in the trash, the internal folder used by immish to store photos that are numbers, are added as albums, you must ensure that the originalPath is inside the external library path.

Also, if you want to modify it to use the last folder nanme in the structrure of folders and not the first one, the change I did is the next one

changed let folder = asset.originalPath.replace(rootPath, "").split("/")[0]; by let segments = asset.originalPath.replace(rootPath, "").split("/"); let folder = segments[segments.length - 2];

Thanks for your script.!

The two modifications from @hectorzin made the script work for me! Thank you very much. I was testing the script with one single folder in the external library. Before having made these changes, an unneccesary internal album "upload" had been generated, and although the (desired) album had been created, there was only an emtpy title. After implementing the two modifications, everything worked well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment