From db7f94f8431677c65166c74e78140b2489f43941 Mon Sep 17 00:00:00 2001 From: nukeop <12746779+nukeop@users.noreply.github.com> Date: Sat, 4 Nov 2023 00:18:59 +0100 Subject: [PATCH] Generate thumbnails in rust --- Cargo.lock | 512 +++++++++++++++++- .../main/src/controllers/local-library.ts | 4 +- .../main/src/services/local-library/index.ts | 4 + packages/scanner/Cargo.toml | 5 + packages/scanner/index.d.ts | 8 +- packages/scanner/src/lib.rs | 19 +- packages/scanner/src/local_track.rs | 2 +- packages/scanner/src/scanner.rs | 17 +- packages/scanner/src/thumbnails.rs | 59 ++ 9 files changed, 614 insertions(+), 16 deletions(-) create mode 100644 packages/scanner/src/thumbnails.rs diff --git a/Cargo.lock b/Cargo.lock index 5b8adf3843..0bee7dda5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,24 +8,73 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbe3c979c178231552ecba20214a8272df4e09f232a87aef4320cf06539aded" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "crc32fast" version = "1.3.2" @@ -35,6 +84,88 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "exr" +version = "1.71.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832a761f35ab3e6664babfbdc6cef35a4860e816ec3916dcfd0882954e98a8a8" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +dependencies = [ + "simd-adler32", +] + [[package]] name = "flate2" version = "1.0.26" @@ -45,6 +176,30 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "spin", +] + +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "getrandom" version = "0.2.10" @@ -56,17 +211,101 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "id3" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9389dd9c8c4671b1e4b2878a6329bccb573f9c24a75bc91c641c451ce5436501" dependencies = [ - "bitflags", + "bitflags 2.3.2", "byteorder", "flate2", ] +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "qoi", + "tiff", + "webp", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.146" @@ -83,6 +322,47 @@ dependencies = [ "winapi", ] +[[package]] +name = "libwebp-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0df0a0f9444d52aee6335cd724d21a2ee3285f646291799a72be518ec8ee3c" +dependencies = [ + "cc", + "glob", +] + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -90,6 +370,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", + "simd-adler32", +] + +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -133,12 +441,91 @@ dependencies = [ "smallvec", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "ppv-lite86" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.60" @@ -148,6 +535,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quote" version = "1.0.28" @@ -187,15 +583,73 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "scanner" version = "0.1.0" dependencies = [ "id3", + "image", + "md5", + "mockall", "neon", "uuid", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "0.9.0" @@ -211,12 +665,27 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "smallvec" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "syn" version = "1.0.109" @@ -239,6 +708,23 @@ dependencies = [ "syn", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "tiff" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d172b0f4d3fba17ba89811858b9d3d97f928aece846475bbda076ca46736211" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "unicode-ident" version = "1.0.9" @@ -261,6 +747,21 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "webp" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb5d8e7814e92297b0e1c773ce43d290bef6c17452dafd9fc49e5edb5beba71" +dependencies = [ + "libwebp-sys", +] + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + [[package]] name = "winapi" version = "0.3.9" @@ -282,3 +783,12 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/packages/main/src/controllers/local-library.ts b/packages/main/src/controllers/local-library.ts index 84b98eb28b..97a3c90f21 100644 --- a/packages/main/src/controllers/local-library.ts +++ b/packages/main/src/controllers/local-library.ts @@ -54,7 +54,7 @@ class LocalIpcCtrl { .map(folder => this.localLibraryDb.addFolder(this.normalizeFolderPath(folder))) ); - const cache = await scanFolders(directories, ['mp3'], (scanProgress, scanTotal) => { + const cache = await scanFolders(directories, ['mp3'], this.localLibrary.getThumbnailsDir(), (scanProgress, scanTotal) => { this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal}); }); @@ -84,7 +84,7 @@ class LocalIpcCtrl { try { const folders = await this.localLibraryDb.getLocalFolders(); - const cache = await scanFolders(folders.map(folder => folder.path), ['mp3'], (scanProgress, scanTotal) => { + const cache = await scanFolders(folders.map(folder => folder.path), ['mp3'], this.localLibrary.getThumbnailsDir(), (scanProgress, scanTotal) => { this.window.send(IpcEvents.LOCAL_FILES_PROGRESS, {scanProgress, scanTotal}); }); diff --git a/packages/main/src/services/local-library/index.ts b/packages/main/src/services/local-library/index.ts index 09ad0d5729..e875e6c05f 100644 --- a/packages/main/src/services/local-library/index.ts +++ b/packages/main/src/services/local-library/index.ts @@ -53,6 +53,10 @@ class LocalLibrary { } } + getThumbnailsDir() { + return this.mediaDir; + } + /** * Format metadata from files to nuclear format */ diff --git a/packages/scanner/Cargo.toml b/packages/scanner/Cargo.toml index 6d5daed87c..b622f93c15 100644 --- a/packages/scanner/Cargo.toml +++ b/packages/scanner/Cargo.toml @@ -10,8 +10,13 @@ crate-type = ["cdylib"] [dependencies] id3 = "1.7.0" +md5 = "0.7.0" mockall = "0.11.4" +[dependencies.image] +version = "0.24.7" +features = ["webp-encoder"] + [dependencies.uuid] version = "1.3.4" features = [ diff --git a/packages/scanner/index.d.ts b/packages/scanner/index.d.ts index 135c9471ea..96b0ac9158 100644 --- a/packages/scanner/index.d.ts +++ b/packages/scanner/index.d.ts @@ -4,7 +4,7 @@ export type LocalTrack = { title?: string; album?: string; duration?: number; - thumbnail?: Buffer; + thumbnail?: string; position?: number; year?: string; @@ -16,6 +16,10 @@ export type LocalTrack = { declare const scanFolders = ( folders: string[], supportedFormats: string[], + thumbnailsDir: string, onProgress: (progress: number, total: number, lastScanned?: string) => void ) => new Promise; -export { scanFolders }; + +declare const generateThumbnail = (filename: string, thumbnailsDir: string) => new Promise; + +export { scanFolders, generateThumbnail }; diff --git a/packages/scanner/src/lib.rs b/packages/scanner/src/lib.rs index 406bec14a2..f21afdf4a8 100644 --- a/packages/scanner/src/lib.rs +++ b/packages/scanner/src/lib.rs @@ -2,16 +2,20 @@ mod error; mod js; mod local_track; mod scanner; +mod thumbnails; use id3::Tag; -use js::{set_optional_field_buffer, set_optional_field_str, set_optional_field_u32}; +use js::{set_optional_field_str, set_optional_field_u32}; use neon::prelude::*; use scanner::{visit_directory, visit_file}; use std::collections::LinkedList; +use thumbnails::create_thumbnails_dir; fn scan_folders(mut cx: FunctionContext) -> JsResult { let folders: Handle = cx.argument(0)?; let supported_formats: Handle = cx.argument(1)?; - let on_progress_callback: Handle = cx.argument(2)?; + let thumbnails_dir: Handle = cx.argument(2)?; + let thumbnails_dir_str = thumbnails_dir.value(&mut cx); + let on_progress_callback: Handle = cx.argument(3)?; let result: Handle = cx.empty_array(); // Copy all the starting folders to a queue, which holds all the folders left to scan @@ -52,6 +56,9 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult { on_progress_callback.call(&mut cx, this, args)?; } + // First, create a directory for thumbnails + create_thumbnails_dir(thumbnails_dir_str.as_str()); + // All folders have been scanned, now scan the files total_files_to_scan_num = files_to_scan_queue.len(); while !files_to_scan_queue.is_empty() { @@ -59,7 +66,11 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult { let file = files_to_scan_queue.pop_front().unwrap(); // Scan the file - let track = visit_file(file.clone(), |path| Tag::read_from_path(path)); + let track = visit_file( + file.clone(), + |path| Tag::read_from_path(path), + thumbnails_dir_str.as_str(), + ); if track.is_err() { // Call the progress callback @@ -89,7 +100,7 @@ fn scan_folders(mut cx: FunctionContext) -> JsResult { let track_duration_js_number = cx.number(track.duration); track_js_object.set(&mut cx, "duration", track_duration_js_number)?; - set_optional_field_buffer(&mut cx, &mut track_js_object, "thumbnail", track.thumbnail); + set_optional_field_str(&mut cx, &mut track_js_object, "thumbnail", track.thumbnail); set_optional_field_u32(&mut cx, &mut track_js_object, "position", track.position); set_optional_field_u32(&mut cx, &mut track_js_object, "disc", track.disc); diff --git a/packages/scanner/src/local_track.rs b/packages/scanner/src/local_track.rs index 12da6cf956..4aeec16cfb 100644 --- a/packages/scanner/src/local_track.rs +++ b/packages/scanner/src/local_track.rs @@ -5,7 +5,7 @@ pub struct LocalTrack { pub title: Option, pub album: Option, pub duration: u32, - pub thumbnail: Option>, + pub thumbnail: Option, pub disc: Option, pub position: Option, pub year: Option, diff --git a/packages/scanner/src/scanner.rs b/packages/scanner/src/scanner.rs index 11b77909c7..42e54ed0e7 100644 --- a/packages/scanner/src/scanner.rs +++ b/packages/scanner/src/scanner.rs @@ -5,12 +5,17 @@ use uuid::Uuid; use crate::error::ScannerError; use crate::local_track::LocalTrack; +use crate::thumbnails::generate_thumbnail; pub trait TagReader { fn read_from_path(path: impl AsRef) -> Result; } -pub fn visit_file(path: String, tag_reader: F) -> Result +pub fn visit_file( + path: String, + tag_reader: F, + thumbnails_dir: &str, +) -> Result where F: FnOnce(&str) -> Result, { @@ -23,10 +28,7 @@ where title: tag.title().map(|s| s.to_string()), album: tag.album().map(|s| s.to_string()), duration: tag.duration().unwrap_or(0), - thumbnail: tag - .pictures() - .find(|p| p.picture_type == id3::frame::PictureType::CoverFront) - .map(|p| p.data.clone()), + thumbnail: generate_thumbnail(&path, thumbnails_dir), position: tag.track(), disc: tag.disc(), year: tag.year().map(|s| s as u32), @@ -110,7 +112,10 @@ mod tests { assert_eq!(track.year, Some(2020)); assert_eq!(track.filename, String::from("file.mp3")); assert_eq!(track.path, path); - assert_eq!(track.thumbnail, Some(vec![1, 2, 3])); + assert_eq!( + track.thumbnail, + Some("file://path/to/valid/file.webp".to_string()) + ); } else { panic!("Result is not ok"); } diff --git a/packages/scanner/src/thumbnails.rs b/packages/scanner/src/thumbnails.rs new file mode 100644 index 0000000000..bd7951c30d --- /dev/null +++ b/packages/scanner/src/thumbnails.rs @@ -0,0 +1,59 @@ +use id3::Tag; +use image::{imageops::resize, imageops::FilterType, io::Reader as ImageReader, ImageFormat}; +use md5; +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +fn hash_thumb_filename(path: &str) -> String { + let filename = Path::new(path).file_name().unwrap(); + let hash = md5::compute(filename.to_string_lossy().as_bytes()); + format!("{:x}.webp", hash) +} + +pub fn create_thumbnails_dir(thumbnails_dir: &str) { + let thumbnails_dir_path = Path::new(thumbnails_dir); + + if !thumbnails_dir_path.exists() { + std::fs::create_dir(thumbnails_dir_path).unwrap(); + } +} + +fn url_path_from_path(path: &str) -> String { + let path = path.replace("\\", "/"); + let path = path.replace(" ", "%20"); + format!("file://{}", path) +} + +pub fn generate_thumbnail(filename: &str, thumbnails_dir: &str) -> Option { + let mut thumbnail_path = PathBuf::from(thumbnails_dir); + + thumbnail_path.push(hash_thumb_filename(filename)); + + let thumbnail_path_str = thumbnail_path.to_str().unwrap(); + + if Path::new(thumbnail_path_str).exists() { + return Some(url_path_from_path(thumbnail_path_str)); + } + + let tag = Tag::read_from_path(filename).unwrap(); + let thumbnail = tag + .pictures() + .find(|p| p.picture_type == id3::frame::PictureType::CoverFront) + .map(|p| p.data.clone()); + + if let Some(thumbnail) = thumbnail { + let img = ImageReader::new(Cursor::new(&thumbnail)) + .with_guessed_format() + .unwrap() + .decode() + .unwrap(); + + let img = resize(&img, 192, 192, FilterType::Lanczos3); + img.save_with_format(thumbnail_path_str, ImageFormat::WebP) + .unwrap(); + } else { + return None; + } + + Some(url_path_from_path(thumbnail_path_str)) +}