class Media { #date = 0; md5sum = ""; fixedSum = ""; path = ""; fileName = ""; meta = {}; fixedTags = []; version = 0; tags = []; writeAccess = false; thumbnail = ""; original = ""; ui = null; constructor(data) { this.#date = data.date; this.md5sum = data.md5sum; this.fixedSum = data.fixedSum; this.path = data.path; this.fileName = data.fileName; this.meta = data.meta || {}; this.fixedTags = []; this.version = data.version; this.tags = []; this.writeAccess = data.writeAccess !== undefined ? data.writeAccess : (data.accessType === 2); this.thumbnail = `/api/media/thumbnail/${data.fixedSum}.jpg`; this.original = `/api/media/original/${data.fixedSum}`; this.ui = null; this.setTags(data.fixedTags || [], data.tags || []); for (let i in this.meta) { if (this.meta[i].type === 'date') this.meta[i].value = new Date(parseInt(this.meta[i].value)); else if (this.meta[i].type === 'number' || this.meta[i].type === 'octet') this.meta[i].value = parseInt(this.meta[i].value); else if (this.meta[i].type === 'string') this.meta[i].value = '' + this.meta[i].value; } } getDateTs() { return this.#date; } getDate() { return new Date(this.#date); } resize(maxWidth, maxHeight) { let ratio = Math.min(1, Math.max( maxWidth / (this.meta?.width?.value || maxWidth), maxHeight / (this.meta?.height?.value || maxHeight))); let result = { width: Math.floor(this.meta.width?.value *ratio), height: Math.floor(this.meta.height?.value *ratio), }; if (isNaN(result.width) || isNaN(result.height) || !result.height || !result.width) { console.error("Failed to resize image ", this); return null; } return result; } setTags(fixedTags, tags) { this.tags = tags.reduce((acc, tag) => { acc.add(tag.replaceAll(/\/\/+/gi, '/')); return acc; }, new Set()); this.fixedTags = fixedTags.reduce((acc, tag) => { acc.add(tag.replaceAll(/\/\/+/gi, '/')); return acc; }, new Set()); } allTags() { return Array.from(new Set([...this.fixedTags, ...this.tags])).sort(); } } function tryLoadMedia(md5sum) { return new Promise((ok, ko) => { $.get("/api/media/" +md5sum, data => { let item = new Media(data); MediaStorage.Instance.pushAll([item], true); ok(item); }).fail(err => { console.error("Trying to get media with md5sum " +md5sum +" failed:", err.responseText); ok(null); }); }); } class MediaStorage extends EventTarget { allMeta = {}; allMetaTypes = {}; allTags = new Set(); medias = []; oldest = null; newest = null; #dbVersion = 0; loadingVersion = 0; constructor() { super(); this.#reset(); window.FilterManager.addEventListener('filterUpdated', () => this.dispatchEvent(new CustomEvent("rebuildMedia"))); } #reset() { this.allMeta = {}; this.allMetaTypes = {}; this.allTags = new Set(); this.medias = []; this.oldest = null; this.newest = null; this.#dbVersion = 0; this.loadingVersion = 0; } async rebuildMetaList() { this.#reset(); this.dispatchEvent(new CustomEvent("rebuildMedia")); window.chronology.reset(); this.downloadMetaList(); } update() { this.downloadMetaList(true); } async restoreMetaList() { if (this.isLoading()) return; this.#reset(); this.#isLoading = true; document.getElementById("pch-infiniteScrollLoading").classList.remove("hidden"); let hasData = false; try { await LoadingTasks.push(async () => { let data = await window.indexedData.listMedias(); this.pushAll(data.medias.map(x => new Media(x))); this.#updateDbVersion(data.version); this.#isLoading = false; hasData = !!(data.medias?.length); }); } catch (err) { console.error(err); } this.downloadMetaList(hasData); } #doDownloadMetaList(isUpdate) { this.#isLoading = true; document.getElementById("pch-infiniteScrollLoading").classList.remove("hidden"); LoadingTasks.push(() => { return new Promise(ok => { let chronology = window.chronology.isInitialized() ? "" : "&chronology" let oldest = isUpdate !== true ? (this.oldest?.getDateTs() || 0) : 0; let oldestArg = oldest ? `&from=${oldest}` : ""; let requestCount = 300; $.get(`/api/media/list?count=${requestCount}${chronology}${oldestArg}&version=${this.#dbVersion}`, async data => { this.pushAll(data.data.map(i => new Media(i))); if (data.first || data.last) window.chronology.rebuildRange(data.first, data.last); if (data.maxVersion) this.loadingVersion = parseInt(data.maxVersion); if ((data.data?.length || 0) < requestCount) { this.#isLoading = false; let updated = this.#updateDbVersion(this.loadingVersion); window.ReloadFilters(MediaStorage.Instance, updated); this.dispatchEvent(new CustomEvent("doneLoading")); document.getElementById("pch-infiniteScrollLoading").classList.add("hidden"); } else { this.#doDownloadMetaList(false); } ok(); }); }); }); } downloadMetaList(isUpdate) { if (this.isLoading()) return; this.#doDownloadMetaList(isUpdate); } getDbVersion() { return this.#dbVersion; } #updateDbVersion(version) { let previousVal = this.#dbVersion; this.#dbVersion = Math.max(this.#dbVersion, version); return previousVal !== this.#dbVersion; } #pushMeta(metaKey, metaVal) { if (metaKey === 'dateTime') return; if (!this.allMeta[metaKey]) this.allMeta[metaKey] = new Set(); this.allMeta[metaKey].add(metaVal.value); if (!this.allMetaTypes[metaKey]) this.allMetaTypes[metaKey] = { type: metaVal.type, canBeEmpty: !!this.medias.length, canWrite: metaVal.canWrite }; } #pushTag(tag, first) { while (tag.length && tag.endsWith('/')) tag = tag.substr(0, tag.length -1); this.allTags.add(tag); let index = tag.lastIndexOf('/'); if (index >= 0) this.#pushTag(tag.substr(0, index)); } #pushUnique(media) { for (let i of media.tags) this.#pushTag(i, true); for (let i of media.fixedTags) this.#pushTag(i, true); for (let key in media.meta) this.#pushMeta(key, media.meta[key]); for (let key in this.allMetaTypes) if (!media.meta[key]) this.allMetaTypes[key].canBeEmpty = true; this.medias.push(media); } #isLoading = false; isLoading() { return this.#isLoading; } pushAll(arr, partialLoad) { let reorder = false; let newItems = []; for (let i of arr) { this.loadingVersion = Math.max(this.loadingVersion, i.version); if (partialLoad !== true) { this.oldest = !this.oldest || this.oldest.getDateTs() > i.getDateTs() ? i : this.oldest; this.newest = !this.newest || this.newest.getDateTs() < i.getDateTs() ? i : this.newest; } if (this.medias.length && this.medias[this.medias.length -1].getDateTs() < i.getDateTs()) reorder = true; let previous = this.medias.find(x => x.fixedSum === i.fixedSum); if (previous) { this.medias = this.medias.filter(x => x.fixedSum !== i.fixedSum); i.ui = previous.ui; } else { newItems.push(i); } this.#pushUnique(i); } for (let i of newItems) this.dispatchEvent(new CustomEvent("newMedia", { detail: i })); if (reorder) { this.medias.sort((a, b) => b.getDateTs() - a.getDateTs()); this.dispatchEvent(new CustomEvent("rebuildMedia")); } } getMediaIndex(media) { return this.medias.indexOf(media); } nextMedia(current) { return this.medias[this.getMediaIndex(current) +1]; } previousMedia(current) { return this.medias[this.getMediaIndex(current) -1]; } getMediaBetweenIndexes(a, b) { if (a > b) return this.getMediaBetweenIndexes(b, a); return this.medias.slice(a, b +1); } getMediaBetween(a, b) { let aIndex = this.medias.indexOf(a); let bIndex = this.medias.indexOf(b); if (aIndex < 0 || bIndex < 0 || aIndex === bIndex) return []; return this.getMediaBetweenIndexes(aIndex, bIndex); } removeMedia(md5sums) { let medias = this.medias.filter(x => md5sums.indexOf(x.fixedSum) >= 0); this.medias = this.medias.filter(x => md5sums.indexOf(x.fixedSum) === -1); for (let i of medias) { i.ui && i.ui.root.remove(); this.dispatchEvent(new CustomEvent("mediaRemoved", { detail: i })); } } getMediaLocal(md5sum) { if (!md5sum) return null; return this.medias.find(x => x.fixedSum === md5sum); } async remoteRemove(md5Sums) { for (let md5sum of md5Sums) { await new Promise(ok => { $.ajax({ url: `/api/media/${encodeURIComponent(md5sum)}`, type: "DELETE", success: ok, error: err => { console.error(err); ok() }, }); }); } await indexedData.removeItems(md5Sums); this.removeMedia(md5Sums); } async getMedia(md5sum) { if (!md5sum) return null; let media = this.medias.find(x => x.fixedSum === md5sum); if (media) return media; return await tryLoadMedia(md5sum); } setMetaValue(md5sum, key, value) { let md5arr = undefined; if (Array.isArray(md5sum) && md5sum.length === 1) md5sum = md5sum[0]; if (Array.isArray(md5sum)) { md5arr = md5sum; md5sum = "list"; } return LoadingTasks.push(() => { return new Promise(ok => { let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length; if (mediaCount != (md5arr || [ md5sum ]).length) return ok(false); $.ajax({ url: `/api/media/${encodeURIComponent(md5sum)}/meta/${encodeURIComponent(key)}`, type: "PATCH", data: { value: value, list: md5arr }, success: allData => { allData.forEach(data => { this.medias = this.medias.filter(x => x.fixedSum !== data.fixedSum); this.#pushUnique(new Media(data)); }); window.ReloadFilters(this); ok(true); }, error: err => ok(false), }); }); }); } removeTag(md5sum, tagName) { let md5arr = undefined; if (Array.isArray(md5sum)) { md5arr = md5sum; md5sum = "list"; } return LoadingTasks.push(() => { return new Promise(ok => { let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length; if (mediaCount != (md5arr || [ md5sum ]).length) return ok(false); $.ajax({ url: `/api/media/${encodeURIComponent(md5sum)}/tag/del/${encodeURIComponent(tagName)}`, type: "POST", data: { list: md5arr || [0] }, success: allData => { allData.forEach(data => { let media = this.medias.find(x => x.fixedSum === data.fixedSum); media.setTags(data.fixedTags, data.tags); for (let i of data.tags) this.#pushTag(i, true); for (let i of data.fixedTags) this.#pushTag(i, true); }); ok(true); }, error: err => ok(false), }); }); }); } addTag(md5sum, tagName) { let md5arr = undefined; if (Array.isArray(md5sum)) { md5arr = md5sum; md5sum = "list"; } return LoadingTasks.push(() => { return new Promise(ok => { let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length; if (mediaCount != (md5arr || [ md5sum ]).length) return ok(false); $.ajax({ url: `/api/media/${encodeURIComponent(md5sum)}/tag`, type: "PUT", data: { tag: tagName, list: md5arr }, success: allData => { allData.forEach(data => { let media = this.medias.find(x => x.fixedSum === data.fixedSum); media.setTags(data.fixedTags, data.tags); for (let i of data.tags) this.#pushTag(i, true); for (let i of data.fixedTags) this.#pushTag(i, true); }); ok(true); }, error: err => ok(false), }); }); }); } } MediaStorage.Instance = new MediaStorage(); setInterval(MediaStorage.Instance.update.bind(MediaStorage.Instance), 60000);