1
0

medias.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. class Media {
  2. #date = 0;
  3. md5sum = "";
  4. fixedSum = "";
  5. path = "";
  6. fileName = "";
  7. meta = {};
  8. fixedTags = [];
  9. version = 0;
  10. tags = [];
  11. writeAccess = false;
  12. thumbnail = "";
  13. original = "";
  14. ui = null;
  15. constructor(data) {
  16. this.#date = data.date;
  17. this.md5sum = data.md5sum;
  18. this.fixedSum = data.fixedSum;
  19. this.path = data.path;
  20. this.fileName = data.fileName;
  21. this.meta = data.meta || {};
  22. this.fixedTags = [];
  23. this.version = data.version;
  24. this.tags = [];
  25. this.writeAccess = data.writeAccess !== undefined ? data.writeAccess : (data.accessType === 2);
  26. this.thumbnail = `/api/media/thumbnail/${data.fixedSum}.jpg`;
  27. this.original = `/api/media/original/${data.fixedSum}`;
  28. this.ui = null;
  29. this.setTags(data.fixedTags || [], data.tags || []);
  30. for (let i in this.meta) {
  31. if (this.meta[i].type === 'date')
  32. this.meta[i].value = new Date(parseInt(this.meta[i].value));
  33. else if (this.meta[i].type === 'number' || this.meta[i].type === 'octet')
  34. this.meta[i].value = parseInt(this.meta[i].value);
  35. else if (this.meta[i].type === 'string')
  36. this.meta[i].value = '' + this.meta[i].value;
  37. }
  38. }
  39. getDateTs() {
  40. return this.#date;
  41. }
  42. getDate() {
  43. return new Date(this.#date);
  44. }
  45. resize(maxWidth, maxHeight) {
  46. let ratio = Math.min(1, Math.max(
  47. maxWidth / (this.meta?.width?.value || maxWidth),
  48. maxHeight / (this.meta?.height?.value || maxHeight)));
  49. let result = {
  50. width: Math.floor(this.meta.width?.value *ratio),
  51. height: Math.floor(this.meta.height?.value *ratio),
  52. };
  53. if (isNaN(result.width) || isNaN(result.height) || !result.height || !result.width) {
  54. console.error("Failed to resize image ", this);
  55. return null;
  56. }
  57. return result;
  58. }
  59. setTags(fixedTags, tags) {
  60. this.tags = tags.reduce((acc, tag) => { acc.add(tag.replaceAll(/\/\/+/gi, '/')); return acc; }, new Set());
  61. this.fixedTags = fixedTags.reduce((acc, tag) => { acc.add(tag.replaceAll(/\/\/+/gi, '/')); return acc; }, new Set());
  62. }
  63. allTags() {
  64. return Array.from(new Set([...this.fixedTags, ...this.tags])).sort();
  65. }
  66. }
  67. function tryLoadMedia(md5sum) {
  68. return new Promise((ok, ko) => {
  69. $.get("/api/media/" +md5sum, data => {
  70. let item = new Media(data);
  71. MediaStorage.Instance.pushAll([item], true);
  72. ok(item);
  73. }).fail(err => {
  74. console.error("Trying to get media with md5sum " +md5sum +" failed:", err.responseText);
  75. ok(null);
  76. });
  77. });
  78. }
  79. class MediaStorage extends EventTarget
  80. {
  81. allMeta = {};
  82. allMetaTypes = {};
  83. allTags = new Set();
  84. medias = [];
  85. oldest = null;
  86. newest = null;
  87. #dbVersion = 0;
  88. loadingVersion = 0;
  89. constructor() {
  90. super();
  91. this.#reset();
  92. window.FilterManager.addEventListener('filterUpdated', () => this.dispatchEvent(new CustomEvent("rebuildMedia")));
  93. }
  94. #reset() {
  95. this.allMeta = {};
  96. this.allMetaTypes = {};
  97. this.allTags = new Set();
  98. this.medias = [];
  99. this.oldest = null;
  100. this.newest = null;
  101. this.#dbVersion = 0;
  102. this.loadingVersion = 0;
  103. }
  104. async rebuildMetaList() {
  105. this.#reset();
  106. this.dispatchEvent(new CustomEvent("rebuildMedia"));
  107. window.chronology.reset();
  108. this.downloadMetaList();
  109. }
  110. update() {
  111. this.downloadMetaList(true);
  112. }
  113. async restoreMetaList() {
  114. if (this.isLoading())
  115. return;
  116. this.#reset();
  117. this.#isLoading = true;
  118. document.getElementById("pch-infiniteScrollLoading").classList.remove("hidden");
  119. let hasData = false;
  120. try {
  121. await LoadingTasks.push(async () => {
  122. let data = await window.indexedData.listMedias();
  123. this.pushAll(data.medias.map(x => new Media(x)));
  124. this.#updateDbVersion(data.version);
  125. this.#isLoading = false;
  126. hasData = !!(data.medias?.length);
  127. });
  128. } catch (err) {
  129. console.error(err);
  130. }
  131. this.downloadMetaList(hasData);
  132. }
  133. #doDownloadMetaList(isUpdate) {
  134. this.#isLoading = true;
  135. document.getElementById("pch-infiniteScrollLoading").classList.remove("hidden");
  136. LoadingTasks.push(() => {
  137. return new Promise(ok => {
  138. let chronology = window.chronology.isInitialized() ? "" : "&chronology"
  139. let oldest = isUpdate !== true ? (this.oldest?.getDateTs() || 0) : 0;
  140. let oldestArg = oldest ? `&from=${oldest}` : "";
  141. let requestCount = 300;
  142. $.get(`/api/media/list?count=${requestCount}${chronology}${oldestArg}&version=${this.#dbVersion}`, async data => {
  143. this.pushAll(data.data.map(i => new Media(i)));
  144. if (data.first || data.last)
  145. window.chronology.rebuildRange(data.first, data.last);
  146. if (data.maxVersion)
  147. this.loadingVersion = parseInt(data.maxVersion);
  148. if ((data.data?.length || 0) < requestCount) {
  149. this.#isLoading = false;
  150. this.#updateDbVersion(this.loadingVersion);
  151. window.ReloadFilters(MediaStorage.Instance);
  152. this.dispatchEvent(new CustomEvent("doneLoading"));
  153. document.getElementById("pch-infiniteScrollLoading").classList.add("hidden");
  154. } else {
  155. this.#doDownloadMetaList(false);
  156. }
  157. ok();
  158. });
  159. });
  160. });
  161. }
  162. downloadMetaList(isUpdate) {
  163. if (this.isLoading())
  164. return;
  165. this.#doDownloadMetaList(isUpdate);
  166. }
  167. getDbVersion() { return this.#dbVersion; }
  168. #updateDbVersion(version) {
  169. this.#dbVersion = Math.max(this.#dbVersion, version);
  170. }
  171. #pushMeta(metaKey, metaVal) {
  172. if (metaKey === 'dateTime')
  173. return;
  174. if (!this.allMeta[metaKey])
  175. this.allMeta[metaKey] = new Set();
  176. this.allMeta[metaKey].add(metaVal.value);
  177. if (!this.allMetaTypes[metaKey])
  178. this.allMetaTypes[metaKey] = { type: metaVal.type, canBeEmpty: !!this.medias.length, canWrite: metaVal.canWrite };
  179. }
  180. #pushTag(tag, first) {
  181. while (tag.length && tag.endsWith('/'))
  182. tag = tag.substr(0, tag.length -1);
  183. this.allTags.add(tag);
  184. let index = tag.lastIndexOf('/');
  185. if (index >= 0)
  186. this.#pushTag(tag.substr(0, index));
  187. }
  188. #pushUnique(media) {
  189. for (let i of media.tags)
  190. this.#pushTag(i, true);
  191. for (let i of media.fixedTags)
  192. this.#pushTag(i, true);
  193. for (let key in media.meta)
  194. this.#pushMeta(key, media.meta[key]);
  195. for (let key in this.allMetaTypes)
  196. if (!media.meta[key])
  197. this.allMetaTypes[key].canBeEmpty = true;
  198. this.medias.push(media);
  199. }
  200. #isLoading = false;
  201. isLoading() { return this.#isLoading; }
  202. pushAll(arr, partialLoad) {
  203. let reorder = false;
  204. let newItems = [];
  205. for (let i of arr) {
  206. this.loadingVersion = Math.max(this.loadingVersion, i.version);
  207. if (partialLoad !== true) {
  208. this.oldest = !this.oldest || this.oldest.getDateTs() > i.getDateTs() ? i : this.oldest;
  209. this.newest = !this.newest || this.newest.getDateTs() < i.getDateTs() ? i : this.newest;
  210. }
  211. if (this.medias.length && this.medias[this.medias.length -1].getDateTs() < i.getDateTs())
  212. reorder = true;
  213. let previous = this.medias.find(x => x.fixedSum === i.fixedSum);
  214. if (previous) {
  215. this.medias = this.medias.filter(x => x.fixedSum !== i.fixedSum);
  216. i.ui = previous.ui;
  217. } else {
  218. newItems.push(i);
  219. }
  220. this.#pushUnique(i);
  221. }
  222. for (let i of newItems)
  223. this.dispatchEvent(new CustomEvent("newMedia", { detail: i }));
  224. if (reorder) {
  225. this.medias.sort((a, b) => b.getDateTs() - a.getDateTs());
  226. this.dispatchEvent(new CustomEvent("rebuildMedia"));
  227. }
  228. }
  229. getMediaIndex(media) {
  230. return this.medias.indexOf(media);
  231. }
  232. nextMedia(current) {
  233. return this.medias[this.getMediaIndex(current) +1];
  234. }
  235. previousMedia(current) {
  236. return this.medias[this.getMediaIndex(current) -1];
  237. }
  238. getMediaBetweenIndexes(a, b) {
  239. if (a > b)
  240. return this.getMediaBetweenIndexes(b, a);
  241. return this.medias.slice(a, b +1);
  242. }
  243. getMediaBetween(a, b) {
  244. let aIndex = this.medias.indexOf(a);
  245. let bIndex = this.medias.indexOf(b);
  246. if (aIndex < 0 || bIndex < 0 || aIndex === bIndex)
  247. return [];
  248. return this.getMediaBetweenIndexes(aIndex, bIndex);
  249. }
  250. removeMedia(md5sums) {
  251. let medias = this.medias.filter(x => md5sums.indexOf(x.fixedSum) >= 0);
  252. this.medias = this.medias.filter(x => md5sums.indexOf(x.fixedSum) === -1);
  253. for (let i of medias) {
  254. i.ui && i.ui.root.remove();
  255. this.dispatchEvent(new CustomEvent("mediaRemoved", { detail: i }));
  256. }
  257. }
  258. getMediaLocal(md5sum) {
  259. if (!md5sum)
  260. return null;
  261. return this.medias.find(x => x.fixedSum === md5sum);
  262. }
  263. async remoteRemove(md5Sums) {
  264. for (let md5sum of md5Sums) {
  265. await new Promise(ok => {
  266. $.ajax({
  267. url: `/api/media/${encodeURIComponent(md5sum)}`,
  268. type: "DELETE",
  269. success: ok,
  270. error: err => { console.error(err); ok() },
  271. });
  272. });
  273. }
  274. await indexedData.removeItems(md5Sums);
  275. this.removeMedia(md5Sums);
  276. }
  277. async getMedia(md5sum) {
  278. if (!md5sum)
  279. return null;
  280. let media = this.medias.find(x => x.fixedSum === md5sum);
  281. if (media)
  282. return media;
  283. return await tryLoadMedia(md5sum);
  284. }
  285. setMetaValue(md5sum, key, value) {
  286. let md5arr = undefined;
  287. if (Array.isArray(md5sum) && md5sum.length === 1)
  288. md5sum = md5sum[0];
  289. if (Array.isArray(md5sum)) {
  290. md5arr = md5sum;
  291. md5sum = "list";
  292. }
  293. return LoadingTasks.push(() => {
  294. return new Promise(ok => {
  295. let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length;
  296. if (mediaCount != (md5arr || [ md5sum ]).length)
  297. return ok(false);
  298. $.ajax({
  299. url: `/api/media/${encodeURIComponent(md5sum)}/meta/${encodeURIComponent(key)}`,
  300. type: "PATCH",
  301. data: { value: value, list: md5arr },
  302. success: allData => {
  303. allData.forEach(data => {
  304. this.medias = this.medias.filter(x => x.fixedSum !== data.fixedSum);
  305. this.#pushUnique(new Media(data));
  306. });
  307. window.ReloadFilters(this);
  308. ok(true);
  309. },
  310. error: err => ok(false),
  311. });
  312. });
  313. });
  314. }
  315. removeTag(md5sum, tagName) {
  316. let md5arr = undefined;
  317. if (Array.isArray(md5sum)) {
  318. md5arr = md5sum;
  319. md5sum = "list";
  320. }
  321. return LoadingTasks.push(() => {
  322. return new Promise(ok => {
  323. let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length;
  324. if (mediaCount != (md5arr || [ md5sum ]).length)
  325. return ok(false);
  326. $.ajax({
  327. url: `/api/media/${encodeURIComponent(md5sum)}/tag/del/${encodeURIComponent(tagName)}`,
  328. type: "POST",
  329. data: { list: md5arr || [0] },
  330. success: allData => {
  331. allData.forEach(data => {
  332. let media = this.medias.find(x => x.fixedSum === data.fixedSum);
  333. media.setTags(data.fixedTags, data.tags);
  334. for (let i of data.tags)
  335. this.#pushTag(i, true);
  336. for (let i of data.fixedTags)
  337. this.#pushTag(i, true);
  338. });
  339. ok(true);
  340. },
  341. error: err => ok(false),
  342. });
  343. });
  344. });
  345. }
  346. addTag(md5sum, tagName) {
  347. let md5arr = undefined;
  348. if (Array.isArray(md5sum)) {
  349. md5arr = md5sum;
  350. md5sum = "list";
  351. }
  352. return LoadingTasks.push(() => {
  353. return new Promise(ok => {
  354. let mediaCount = (md5arr || [ md5sum ]).map(checksum => this.medias.find(x => x.fixedSum === checksum)).filter(x => x.writeAccess).length;
  355. if (mediaCount != (md5arr || [ md5sum ]).length)
  356. return ok(false);
  357. $.ajax({
  358. url: `/api/media/${encodeURIComponent(md5sum)}/tag`,
  359. type: "PUT",
  360. data: { tag: tagName, list: md5arr },
  361. success: allData => {
  362. allData.forEach(data => {
  363. let media = this.medias.find(x => x.fixedSum === data.fixedSum);
  364. media.setTags(data.fixedTags, data.tags);
  365. for (let i of data.tags)
  366. this.#pushTag(i, true);
  367. for (let i of data.fixedTags)
  368. this.#pushTag(i, true);
  369. });
  370. ok(true);
  371. },
  372. error: err => ok(false),
  373. });
  374. });
  375. });
  376. }
  377. }
  378. MediaStorage.Instance = new MediaStorage();
  379. setInterval(MediaStorage.Instance.update.bind(MediaStorage.Instance), 60000);