All files / lib/photos-library/model album.ts

100% Statements 211/211
94.59% Branches 35/37
100% Functions 13/13
100% Lines 211/211

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 2111x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 380x 149x 149x 380x 380x 380x 380x 380x 380x 52x 52x 380x 380x 380x 380x 380x 380x 1922x 1922x 380x 380x 380x 380x 380x 380x 380x 8x 8x 8x 8x 8x 8x 8x 8x 8x 380x 380x 380x 380x 380x 380x 12x 12x 380x 380x 380x 380x 380x 380x 3x 3x 380x 380x 380x 380x 380x 380x 380x 14x 14x 14x 14x 14x 14x 14x 13x 13x 14x 14x 14x 380x 380x 380x 380x 380x 380x 380x 13x 13x 13x 13x 13x 380x 380x 380x 380x 380x 380x 380x 23x 9x 9x 14x 14x 14x 14x 380x 380x 380x 380x 380x 380x 380x 380x 384x 179x 179x 205x 384x 62x 62x 143x 143x 143x 143x 341x 138x 138x 5x 5x 5x 5x 380x 380x 380x 380x 380x 380x 380x 380x 380x 234x 116x 116x 118x 118x 230x 113x 113x 5x 5x 5x 380x
import {jsonc} from "jsonc";
import {LIBRARY_ERR} from "../../../app/error/error-codes.js";
import {iCPSError} from "../../../app/error/error.js";
import {CPLAlbum} from "../../icloud/icloud-photos/query-parser.js";
import {STASH_DIR} from "../constants.js";
import {PEntity} from "./photos-entity.js";
 
/**
 * Potential AlbumTypes
 */
export enum AlbumType {
    FOLDER = 3,
    ALBUM = 0,
    ARCHIVED = 99
}
 
/**
 * This type is mapping the filename in the asset folder, to the filename how the asset should be presented to the user
 * Key: Asset.getAssetFilename, Value: Asset.getPrettyFilename
 */
export type AlbumAssets = {
    [key: string]: string
}
 
/**
 * This class represents a photo album within the library
 */
export class Album implements PEntity<Album> {
    /**
     * UUID of this album
     */
    uuid: string;
    /**
     * Album type of this album
     */
    albumType: AlbumType;
    /**
     * The name of this album
     */
    albumName: string;
    /**
     * Assets, where the key is the uuid & the value the filename
     */
    assets: AlbumAssets;
    /**
     * UUID of parent folder
     */
    parentAlbumUUID: string;
 
    /**
     * Constructs a new album
     * @param uuid - The UUID of the album
     * @param albumType - The album type of the album
     * @param albumName - The album name of the album
     * @param parentAlbumUUID - The UUID of the parent album
     */
    constructor(uuid: string, albumType: AlbumType, albumName: string, parentAlbumUUID: string) {
        this.uuid = uuid;
        this.albumType = albumType;
        this.albumName = albumName;
        this.parentAlbumUUID = parentAlbumUUID;
        this.assets = {};
    }
 
    /**
     *
     * @returns The display name of this album instance
     */
    getDisplayName(): string {
        return this.albumName;
    }
 
    /**
     *
     * @returns A valid filename, that will be used to store the album on disk
     */
    getSanitizedFilename(): string {
        return this.albumName.replaceAll(`/`, `_`);
    }
 
    /**
     *
     * @returns The UUID of this album instance
     */
    getUUID(): string {
        return this.uuid;
    }
 
    /**
     * Creates an album from a CPLAlbum instance (as returned from the backend)
     * @param cplAlbum - The album retrieved from the backend
     * @returns An Album based on the CPL object
     */
    static fromCPL(cplAlbum: CPLAlbum): Album {
        const album = new Album(
            cplAlbum.recordName,
            cplAlbum.albumType,
            Buffer.from(cplAlbum.albumNameEnc, `base64`).toString(`utf8`),
            cplAlbum.parentId ? cplAlbum.parentId : ``,
        );
        album.assets = cplAlbum.assets ?? {};
        return album;
    }
 
    /**
     * Creates a dummy root album that is used to load all other albums from disk
     * @returns The dummy root album
     */
    static getRootAlbum(): Album {
        return new Album(``, AlbumType.FOLDER, `iCloud Photos Library`, ``);
    }
 
    /**
     * Creates a dummy stash album tat is used to load all albums currently within the stash album
     * @returns The dummy stash album
     */
    static getStashAlbum(): Album {
        return new Album(STASH_DIR, AlbumType.FOLDER, `iCloud Photos Library Archive`, ``);
    }
 
    /**
     *
     * @param album - An album to compare to this instance
     * @returns True if provided album is equal to this instance (based on UUID, AlbumType, AlbumName, Parent UUID & list of associated assets) - In case either of the albumTypes is archived, the list of assets will be ignored
     */
    equal(album: Album): boolean {
        return album
            && this.uuid === album.uuid
            && this.getSanitizedFilename() === album.getSanitizedFilename()
            && this.parentAlbumUUID === album.parentAlbumUUID
            && ( // If any of the albumTypes is ARCHIVED we will ignore asset and albumType equality
                this.albumType === AlbumType.ARCHIVED || album.albumType === AlbumType.ARCHIVED
                || (
                    this.assetsEqual(album.assets)
                    && this.albumType === album.albumType
                )
            );
    }
 
    /**
     * Checks of the assets, attached to this album are equal to the provided assets
     * @param assets - The list of assets to compare to
     * @returns True, if the assets are equal (order does not matter)
     */
    assetsEqual(assets: AlbumAssets) {
        // Assets might be undefined
        const thisAssets = this.assets ? this.assets : {};
        const otherAssets = assets ? assets : {};
        return jsonc.stringify(Object.keys(thisAssets).sort()) === jsonc.stringify(Object.keys(otherAssets).sort());
    }
 
    /**
     * Should only be called on a 'remote' entity. Will apply the local entity's properties to the remote one
     * @param localEntity - The local entity
     * @returns This object with the applied properties
     */
    apply(localEntity: Album): Album {
        if (!localEntity) {
            return this;
        }
 
        this.albumType = localEntity.albumType;
        return this;
    }
 
    /**
     * Check if a given album is in the chain of ancestors
     * @param potentialAncestor - The potential ancestor for the given album
     * @param fullQueue - The full list of albums
     * @returns True if potentialAncestor is part of this album's directory tree
     */
    hasAncestor(potentialAncestor: Album, fullQueue: Album[]): boolean {
        if (this.parentAlbumUUID === ``) { // If this is a root album, the potentialAncestor cannot be a ancestor
            return false;
        }
 
        if (potentialAncestor.getUUID() === this.parentAlbumUUID) { // If the ancestor is the parent, return true
            return true;
        }
 
        // Find actual parent
        const parent = fullQueue.find(album => album.getUUID() === this.parentAlbumUUID);
        // If there is a parent, check if it has the ancestor
        if (parent) {
            return parent.hasAncestor(potentialAncestor, fullQueue);
        }
 
        // If there is no parent, this means the queue has a gap and all we can assume is, that the ancestor is false
        return false;
    }
 
    /**
     * Calculates the distance to the root folder in order to compare the album's order.
     * @param album - The album, whose depth needs to be calculated
     * @param fullQueue - The list of all albums
     * @returns The number of albums between the given album and the root album
     * @throws An iCPSError, in case there is no link from the given album to root
     */
    static distanceToRoot(album: Album, fullQueue: Album[]): number {
        if (album.parentAlbumUUID === ``) {
            return 0;
        }
 
        const parent = fullQueue.find(potentialParent => potentialParent.getUUID() === album.parentAlbumUUID);
        if (parent) {
            return Album.distanceToRoot(parent, fullQueue) + 1;
        }
 
        throw new iCPSError(LIBRARY_ERR.NO_DISTANCE_TO_ROOT);
    }
}