From 2b2639287395834ec43442926a592f6b4150342f Mon Sep 17 00:00:00 2001 From: Ashhhleyyy Date: Thu, 15 Jun 2023 19:32:03 +0100 Subject: [PATCH] feat: initial commit --- .envrc | 1 + .gitignore | 5 + Cargo.lock | 789 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 17 + README.md | 13 + flake.lock | 165 ++++++++++ flake.nix | 90 ++++++ src/main.rs | 36 +++ src/nfo.rs | 126 ++++++++ src/style.css | 118 +++++++ src/templates.rs | 201 ++++++++++++ src/util.rs | 23 ++ src/walker.rs | 139 +++++++++ 13 files changed, 1723 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/main.rs create mode 100644 src/nfo.rs create mode 100644 src/style.css create mode 100644 src/templates.rs create mode 100644 src/util.rs create mode 100644 src/walker.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a963ecc --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/target +/result +/.direnv +/out +*.nfo diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..86d4ade --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,789 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "litefin" +version = "0.1.0" +dependencies = [ + "clap", + "color-eyre", + "maud", + "serde", + "serde_json", + "url", + "walkdir", + "yaserde", + "yaserde_derive", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "maud" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0bab19cef8a7fe1c18a43e881793bfc9d4ea984befec3ae5bd0415abf3ecf00" +dependencies = [ + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be95d66c3024ffce639216058e5bae17a83ecaf266ffc6e4d060ad447c9eed2" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "object" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.37.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.163" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.16", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +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 = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "xml-rs" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1690519550bfa95525229b9ca2350c63043a4857b3b0013811b2ccf4a2420b01" + +[[package]] +name = "yaserde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf52af554a50b866aaad63d7eabd6fca298db3dfe49afd50b7ba5a33dfa0582" +dependencies = [ + "log", + "xml-rs", +] + +[[package]] +name = "yaserde_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab8bd5c76eebb8380b26833d30abddbdd885b00dd06178412e0d51d5bfc221f" +dependencies = [ + "heck", + "log", + "proc-macro2", + "quote", + "syn 1.0.109", + "xml-rs", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1a30883 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "litefin" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.3.0", features = ["derive"] } +color-eyre = "0.6.2" +maud = "0.25.0" +serde = { version = "1.0.163", features = ["derive"] } +serde_json = "1.0.96" +url = "2.3.1" +walkdir = "2.3.3" +yaserde = "0.8.0" +yaserde_derive = "0.8.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..1287c3d --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# LiteFin + +Lightweight media player using Jellyfin as a base. + +## Setup + +Import your media library into Jellyfin, and ensure that the `Nfo` "Metadata saver" is enabled when importing; this makes Jellyfin write information about series into the source folder alongside the media files. + +To generate the static HTML pages, use `cargo run -- --base-url --rewrite-root /media/ --output ./out ` + +`` should be replaced with the browser-accessible URL of the ``. `--rewrite-root` is used to convert the image paths in the metadata files to the corresponding browser URL. + +Once the command has completed, you can copy the contents of the output directory to your webserver, to allow the library to easily be browsed. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d6ef021 --- /dev/null +++ b/flake.lock @@ -0,0 +1,165 @@ +{ + "nodes": { + "crane": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "nixpkgs": [ + "nixpkgs" + ], + "rust-overlay": "rust-overlay" + }, + "locked": { + "lastModified": 1680584903, + "narHash": "sha256-uraq+D3jcLzw/UVk0xMHcnfILfIMa0DLrtAEq2nNlxU=", + "owner": "ipetkov", + "repo": "crane", + "rev": "65d3f6a3970cd46bef5eedfd458300f72c56b3c5", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1673956053, + "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "locked": { + "lastModified": 1678901627, + "narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1681037374, + "narHash": "sha256-XL6X3VGbEFJZDUouv2xpKg2Aljzu/etPLv5e1FPt1q0=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "033b9f258ca96a10e543d4442071f614dc3f8412", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1681064623, + "narHash": "sha256-UngFykv8KTrjxFeu4ZMvsOwFrxsa0A3ZPwyLhxb0Rrs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "da0b0bc6a5d699a8a9ffbf9e1b19e8642307062a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay_2" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "crane", + "flake-utils" + ], + "nixpkgs": [ + "crane", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1680488274, + "narHash": "sha256-0vYMrZDdokVmPQQXtFpnqA2wEgCCUXf5a3dDuDVshn0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "7ec2ff598a172c6e8584457167575b3a1a5d80d8", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1681093076, + "narHash": "sha256-6uLZNeuP5jDDGlFkXgcoAxsJhTKy8yUTw25zdLHzdxE=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "45c2ed9dd1397526dad35fc867c43955d87f9f3f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..da1d9aa --- /dev/null +++ b/flake.nix @@ -0,0 +1,90 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + crane.url = "github:ipetkov/crane"; + crane.inputs.nixpkgs.follows = "nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "flake-utils"; + }; + }; + }; + + outputs = { self, nixpkgs, crane, rust-overlay, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + inherit (pkgs) lib; + + rust-toolchain = pkgs.rust-bin.stable.latest.default; + rust-dev-toolchain = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" "rust-analyzer" ]; + }; + + craneLib = (crane.mkLib pkgs).overrideToolchain rust-toolchain; + + cssFilter = path: _type: builtins.match ".*css$" path != null; + cssOrCargo = path: type: (cssFilter path type) || (craneLib.filterCargoSources path type); + src = lib.cleanSourceWith { + src = craneLib.path ./.; + filter = cssOrCargo; + }; + + buildInputs = [ + pkgs.bintools + ] ++ lib.optionals pkgs.stdenv.isDarwin [ + # Additional darwin specific inputs can be set here + pkgs.libiconv + ]; + + cargoArtifacts = craneLib.buildDepsOnly { + inherit src buildInputs; + }; + + # Build the actual crate itself, reusing the dependency + # artifacts from above. + litefin = craneLib.buildPackage { + inherit cargoArtifacts src buildInputs; + }; + in + { + checks = { + # Build the crate as part of `nix flake check` for convenience + inherit litefin; + + # Run clippy (and deny all warnings) on the crate source, + # again, resuing the dependency artifacts from above. + # + # Note that this is done as a separate derivation so that + # we can block the CI if there are issues here, but not + # prevent downstream consumers from building our crate by itself. + litefin-clippy = craneLib.cargoClippy { + inherit cargoArtifacts src buildInputs; + cargoClippyExtraArgs = "--all-targets -- --deny warnings"; + }; + + # Check formatting + litefin-fmt = craneLib.cargoFmt { + inherit src; + }; + }; + + packages.default = litefin; + + devShells.default = pkgs.mkShell { + inputsFrom = builtins.attrValues self.checks; + + # Extra inputs can be added here + nativeBuildInputs = with pkgs; [ + rust-dev-toolchain + cargo-watch + ]; + }; + }); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..86b3760 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use clap::Parser; +use color_eyre::Result; + +mod nfo; +mod templates; +mod util; +mod walker; + +#[derive(clap::Parser)] +#[command(version, about)] +pub struct Args { + /// Root media directory to walk + dir: PathBuf, + + /// Root directory that media files are contained in, used to rewrite paths in nfo files. + #[clap(short, long)] + rewrite_root: String, + + /// URL pointing to the root `dir` + #[clap(short, long)] + base_url: String, + + /// Path to write the generated HTML files to. + #[clap(short, long)] + output: PathBuf, +} + +fn main() -> Result<()> { + let args = Args::parse(); + + walker::walk_metadata(&args)?; + + Ok(()) +} diff --git a/src/nfo.rs b/src/nfo.rs new file mode 100644 index 0000000..79f086a --- /dev/null +++ b/src/nfo.rs @@ -0,0 +1,126 @@ +use yaserde_derive::YaDeserialize; + +#[derive(Clone)] +pub struct ItemMeta { + pub url: String, + pub ty: ContentType, + pub data: ItemData, + pub parent: Option, +} + +impl ItemMeta { + pub fn fanart_url(&self) -> Option<&str> { + if let Some(fanart) = &self.data.art.fanart { + Some(fanart) + } else if let Some(parent) = &self.parent { + if let Some(fanart) = &parent.art.fanart { + Some(fanart) + } else { + None + } + } else { + None + } + } +} + +#[derive(Clone, Debug)] +pub enum ContentType { + TvShow, + Season, + Episode, +} + +impl ContentType { + pub fn is_season(&self) -> bool { + match self { + ContentType::Season => true, + _ => false, + } + } + + pub fn is_episode(&self) -> bool { + match self { + ContentType::Episode => true, + _ => false, + } + } +} + +#[derive(Clone, Debug, Default, YaDeserialize)] +pub struct ItemData { + pub plot: Option, + pub outline: Option, + pub lockdata: bool, + #[yaserde(rename = "dateadded")] + pub date_added: String, + pub title: String, + #[yaserde(rename = "originaltitle")] + pub original_title: Option, + pub trailer: Vec, + pub rating: f64, + pub year: u32, + pub mpaa: Option, + #[yaserde(rename = "imdbid")] + pub imdb_id: Option, + #[yaserde(rename = "tmdbid")] + pub tmdb_id: Option, + #[yaserde(rename = "tvdbid")] + pub tvdb_id: String, + pub premiered: Option, + #[yaserde(rename = "releasedate")] + pub release_date: Option, + #[yaserde(rename = "enddate")] + pub end_date: Option, + pub runtime: u32, + #[yaserde(rename = "genre")] + pub genres: Vec, + pub studio: Option, + #[yaserde(rename = "tag")] + pub tags: Vec, + pub art: Art, + #[yaserde(rename = "actor")] + pub actors: Vec, + pub id: Option, + pub status: Option, + #[yaserde(rename = "seasonnumber")] + pub season_number: Option, + pub episode: Option, +} + +impl ItemData { + pub fn description(&self) -> &str { + if let Some(outline) = &self.outline { + outline + } else if let Some(plot) = &self.plot { + plot + } else { + "" + } + } +} + +#[derive(Clone, Debug, Default, YaDeserialize)] +#[yaserde(rename = "art")] +pub struct Art { + pub poster: Option, + pub fanart: Option, +} + +#[derive(Clone, Debug, Default, YaDeserialize)] +#[yaserde(rename = "actor")] +pub struct Actor { + pub name: String, + pub role: String, + #[yaserde(rename = "type")] + pub ty: String, + #[yaserde(rename = "sortorder")] + pub sort_order: i32, + pub thumb: Option, +} + +pub struct SubtitleTrack { + pub url: String, + pub label: String, + pub lang: String, +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..2bf9c42 --- /dev/null +++ b/src/style.css @@ -0,0 +1,118 @@ +/* AshCSS */ +:root { + --background: #13092b; + --background-transparent: #140a2bee; + --background-2: #090f2b; + --foreground: #ddd; + --foreground-bright: #fff; + --foreground-dim: #aaa; + --error: orange; + --accent: #f9027a; + --accent-dim: hsl(331, 50%, 49%); +} + +html, body { + font-family: 'Poppins', sans-serif; + margin: 0; + padding: 0; + background-color: var(--background); + box-sizing: border-box; +} + +* { + box-sizing: inherit; +} + +h1, h2, h3, h4, h5, h6 { + margin: 0; +} + +a { + color: white; +} + +.fadein { + transition: opacity 500ms ease; +} + +.poster-body { + min-width: 100%; + min-height: 100vh; + /* background-size: cover; + background-position: center; */ +} + +.poster-image { + width: 100%; + position: absolute; + top: 0; + left: 0; + max-height: 100vh; + object-fit: cover; +} + +.poster-details { + position: absolute; + width: 100%; + bottom: 0; + left: 0; + color: var(--foreground); + background: var(--background-transparent); + background: linear-gradient(0deg, var(--background-transparent) 50%, rgba(0,0,0,0) 100%); +} + +.poster-details-gradient { + width: 100%; + height: 256px; +} + +.poster-details-content { + display: flex; + flex-direction: column; + padding: 16px; + max-width: 100vw; + gap: 16px; +} + +.poster-details-content img { + /* display: inline-block; */ + height: 100%; + max-height: 24rem; +} + +.metadata { + display: flex; + flex-direction: row; + gap: 16px; + align-items: flex-end; +} + +.metadata-head { + max-width: 720px; +} + +.seasons { + display: flex; + flex-direction: row; + gap: 16px; + padding-left: 16px; + padding-right: 16px; + max-width: 100%; + overflow: auto; + white-space: nowrap; +} + +.season { + transform-origin: center; + transition: transform 300ms ease; + text-align: center; + margin-top: 1rem; +} + +.season:hover { + transform: translateY(-1rem) scale(1.05); +} + +.season img { + height: 12rem; +} diff --git a/src/templates.rs b/src/templates.rs new file mode 100644 index 0000000..007b2c9 --- /dev/null +++ b/src/templates.rs @@ -0,0 +1,201 @@ +use color_eyre::Result; + +use crate::{ + nfo::{ItemData, ItemMeta, SubtitleTrack}, + util::get_url, + Args, +}; + +fn base_template( + title: impl maud::Render, + body: impl maud::Render, + extra_head: Option, +) -> maud::Markup { + maud::html! { + (maud::DOCTYPE) + html { + head { + meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1.0"; + title { (title) } + link rel="stylesheet" href="https://cdn.ashhhleyyy.dev/file/ashhhleyyy-assets/css/addf63c63146a6d5.css"; + style { (include_str!("style.css")) } + @if let Some(extra_head) = extra_head { + (extra_head) + } + } + body { + (body) + } + } + } +} + +pub fn player( + title: &str, + video_url: &str, + poster: &str, + subtitles: &[SubtitleTrack], + args: &Args, +) -> Result { + Ok(base_template( + title, + maud::html! { + media-player + title=(title) + src=(video_url) + poster=(get_url(poster, args)?) + aspect-ratio="16/9" + style="height: 100vh" + crossorigin { + media-outlet { + media-poster alt=(title) {} + @for subtitle_track in subtitles { + track src=(get_url(&subtitle_track.url, args)?) + label=(subtitle_track.label) + srclang=(subtitle_track.lang) + kind="subtitles" + default; + } + } + media-community-skin {} + } + }, + Some(maud::html! { + link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vidstack/styles/defaults.min.css"; + link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vidstack/styles/community-skin/video.min.css"; + script type="module" src="https://cdn.jsdelivr.net/npm/vidstack/dist/cdn/dev.js" {} + }), + )) +} + +pub fn tv_show(details: &ItemMeta, args: &Args, children: &[ItemMeta]) -> Result { + let fanart_url = details.fanart_url().map(|s| get_url(s, args)).transpose()?; + let poster_url = details + .data + .art + .poster + .as_ref() + .map(|s| get_url(s, args)) + .transpose()?; + Ok(base_template( + &details.data.title, + maud::html! { + .poster-body { + @if let Some(fanart_url) = fanart_url { + (fade_image(&fanart_url, None, None, None, Some("poster-image"))) + // (fade_image("https://placehold.co/1920x1080", None, None, None, Some("poster-image"))) + } + .poster-details { + .poster-details-gradient {} + .poster-details-content { + .metadata { + @if let Some(poster_url) = poster_url { + (fade_image(&poster_url, None, None, None, None)) + // (fade_image("https://placehold.co/1364x2042", None, None, None, None)) + } + .metadata-head { + h1 { (details.data.title) } + p.description { (details.data.description()) } + } + } + .seasons { + @for child in children { + @if child.ty.is_season() { + @match season_card(&child.data, &child.url, args) { + Ok(s) => (s), + Err(_) => {}, + } + } @else if child.ty.is_episode() { + @match episode_card(&child.data, &child.url, args) { + Ok(s) => (s), + Err(_) => {}, + } + } + } + } + } + } + } + }, + None::, + )) +} + +fn season_card(info: &ItemData, url: &str, args: &Args) -> Result { + let poster_url = get_url(&info.art.poster.clone().unwrap(), args)?; + Ok(maud::html! { + .season { + a href=(url) { + (fade_image(&poster_url, None, None, None, None)) + // (fade_image("https://placehold.co/300x200", None, None, None, None)) + .h2 { (info.title) } + } + } + }) +} + +fn episode_card(info: &ItemData, url: &str, args: &Args) -> Result { + let poster_url = get_url(&info.art.poster.clone().unwrap(), args)?; + Ok(maud::html! { + .season { + a href=(url) { + (fade_image(&poster_url, None, None, None, None)) + // (fade_image("https://placehold.co/300x200", None, None, None, None)) + .h2 { (info.title) } + } + } + }) +} + +fn fade_image( + url: &str, + alt: Option<&str>, + width: Option, + height: Option, + class: Option<&str>, +) -> maud::Markup { + #[derive(serde::Serialize)] + struct ImageInfo<'a> { + url: &'a str, + alt: Option<&'a str>, + width: Option, + height: Option, + class: Option<&'a str>, + } + + let data = serde_json::to_string(&ImageInfo { + url, + alt, + width, + height, + class, + }) + .expect("failed to serialise"); + + maud::html! { + noscript { + img src=(url) loading="lazy" width=[width] height=[height] alt=[alt] class=[class]; + } + + script { + (maud::PreEscaped(format!(r#" + (function() {{ + const data = {data}; + const img = document.createElement('img'); + img.src = data.url; + if (data.alt) img.alt = data.alt; + if (data.width) img.width = data.width; + if (data.height) img.height = data.height; + if (data.class) img.className = data.class; + img.classList.add('fadein'); + img.style.opacity = 0; + img.onload = () => {{ + img.style.opacity = 1; + }}; + document.currentScript.parentElement.insertBefore(img, document.currentScript); + }})() + "#))) + } + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..3c79a7c --- /dev/null +++ b/src/util.rs @@ -0,0 +1,23 @@ +use color_eyre::Result; +use url::Url; + +use crate::Args; + +pub(crate) fn get_url(path: &str, args: &Args) -> Result { + let prefix = if args.rewrite_root.ends_with('/') { + args.rewrite_root.clone() + } else { + format!("{}/", args.rewrite_root) + }; + let path = match path.strip_prefix(&prefix) { + Some(path) => path, + None => path, + }; + let url = if args.base_url.ends_with('/') { + format!("{}{path}", args.base_url) + } else { + format!("{}/{path}", args.base_url) + }; + let url = Url::parse(&url)?; + Ok(url.to_string()) +} diff --git a/src/walker.rs b/src/walker.rs new file mode 100644 index 0000000..0f3c4a4 --- /dev/null +++ b/src/walker.rs @@ -0,0 +1,139 @@ +use std::{path::{Path, PathBuf}, fs::File, collections::HashMap}; + +use color_eyre::{Result, eyre::eyre}; + +use crate::{nfo::{ContentType, ItemMeta, ItemData}, Args, templates, util::get_url}; + +pub fn walk_metadata(args: &Args) -> Result<()> { + let mut path_cache = HashMap::::new(); + + let walk = walkdir::WalkDir::new(&args.dir) + .contents_first(true); + + for entry in walk { + let entry = entry?; + if !entry.file_type().is_dir() { continue; } + let path = entry.path(); + let read = std::fs::read_dir(path)?; + let mut entries = Vec::with_capacity(read.size_hint().0); + for e in read { + entries.push(e?.path()); + } + let content_type = determine_contents(path, &entries); + if let Some(content_type) = content_type { + let info = load_contents(path, content_type, args)?; + let output_dir = path.strip_prefix(&args.dir)?; + let output_dir = args.output.join(output_dir); + std::fs::create_dir_all(&output_dir)?; + let output_path = output_dir.join("index.html"); + match info.ty { + ContentType::TvShow => { + let children = find_tv_show_children(&path_cache, &entries); + let rendered = templates::tv_show(&info, args, &children)?; + std::fs::write(output_path, rendered.0)?; + }, + ContentType::Season => { + let mut children = find_season_children(&path_cache, &entries, &info.data, args)?; + + for child in &mut children { + let rendered = templates::player( + &child.data.title, + &child.url, + child.data.art.poster.as_ref().unwrap(), + &[], + args, + )?; + let filename = format!("Episode {}.html", child.data.episode.unwrap()); + std::fs::write(output_dir.join(&filename), rendered.0)?; + child.url = filename; + } + + let rendered = templates::tv_show(&info, args, &children)?; + std::fs::write(output_path, rendered.0)?; + }, + ContentType::Episode => unreachable!(), + } + println!("{path:?}: {:?}", info.ty); + path_cache.insert(path.to_owned(), info); + } else { + println!("{path:?}: no content type"); + } + } + + Ok(()) +} + +fn find_tv_show_children(path_cache: &HashMap, entries: &[PathBuf]) -> Vec { + let mut children = vec![]; + for entry in entries { + if let Some(info) = path_cache.get(entry) { + let mut info = info.clone(); + info.url = entry.file_name().unwrap().to_string_lossy().to_string(); + children.push(info); + } + } + children.sort_by_key(|c| c.data.season_number); + children +} + +fn find_season_children(_path_cache: &HashMap, entries: &[PathBuf], parent: &ItemData, args: &Args) -> Result> { + let mut children = vec![]; + for entry in entries { + if let (Some(stem), Some(ext)) = (entry.file_stem(), entry.extension()) { + if let (Some(stem), Some(ext)) = (stem.to_str(), ext.to_str()) { + if stem.contains("Episode") && ext == "nfo" { + let data = read_data(entry)?; + let video = entry.with_extension("mp4"); + let url = get_url(&video.to_string_lossy().replace("/stuff/ash/Series", "/media/Shows"), args)?; + children.push(ItemMeta { + url, + ty: ContentType::Episode, + data, + parent: Some(parent.clone()), + }); + } + } + } + } + children.sort_by_key(|c| c.data.episode); + Ok(children) +} + +fn read_data(path: &Path) -> Result { + let mut f = File::open(path)?; + let data: ItemData = yaserde::de::from_reader(&mut f).map_err(|e| eyre!("failed to parse directory meta: {e}"))?; + Ok(data) +} + +fn load_contents(path: &Path, content_type: ContentType, args: &Args) -> Result { + let meta_file = match content_type { + ContentType::TvShow => "tvshow.nfo", + ContentType::Season => "season.nfo", + ContentType::Episode => unreachable!(), + }; + let data = read_data(&path.join(meta_file))?; + let url = get_url(&path.to_string_lossy().to_owned(), args)?; + let parent = match content_type { + ContentType::TvShow => None, + ContentType::Season => Some(read_data(&path.parent().unwrap().join("tvshow.nfo"))?), + ContentType::Episode => unreachable!(), + }; + Ok(ItemMeta { + ty: content_type, + data, + url, + parent, + }) +} + +fn determine_contents(_path: &Path, entries: &[PathBuf]) -> Option { + for entry in entries { + if entry.ends_with("tvshow.nfo") { + return Some(ContentType::TvShow); + } + if entry.ends_with("season.nfo") { + return Some(ContentType::Season); + } + } + None +}