diff --git a/Cargo.lock b/Cargo.lock index d01a80c..44a47cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,9 +92,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" [[package]] name = "async-speed-limit" @@ -119,12 +119,6 @@ dependencies = [ "syn", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.3.0" @@ -194,9 +188,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.98" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" [[package]] name = "cfg-if" @@ -244,7 +238,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -349,7 +343,7 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "crunchy-cli" -version = "3.6.7" +version = "3.6.1" dependencies = [ "chrono", "clap", @@ -362,7 +356,7 @@ dependencies = [ [[package]] name = "crunchy-cli-core" -version = "3.6.7" +version = "3.6.1" dependencies = [ "anyhow", "async-speed-limit", @@ -392,7 +386,6 @@ dependencies = [ "shlex", "sys-locale", "tempfile", - "time", "tokio", "tokio-util", "tower-service", @@ -400,9 +393,9 @@ dependencies = [ [[package]] name = "crunchyroll-rs" -version = "0.11.4" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6e38c223aecf65c9c9bec50764beea5dc70b6c97cd7f767bf6860f2fc8e0a07" +checksum = "58580acc9c0abf96a231ec8b1a4597ea55d9426ea17f684ce3582e2b26437bbb" dependencies = [ "async-trait", "chrono", @@ -426,9 +419,9 @@ dependencies = [ [[package]] name = "crunchyroll-rs-internal" -version = "0.11.4" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144a38040a21aaa456741a9f6749354527bb68ad3bb14210e0bbc40fbd95186c" +checksum = "ce3c844dec8a3390f8c9853b5cf1d65c3d38fd0657b8b5d0e008db8945dea326" dependencies = [ "darling", "quote", @@ -447,9 +440,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" dependencies = [ "darling_core", "darling_macro", @@ -457,23 +450,23 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.10.0", "syn", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" dependencies = [ "darling_core", "quote", @@ -482,13 +475,12 @@ dependencies = [ [[package]] name = "dash-mpd" -version = "0.16.3" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4618a5e165bf47b084963611bcf1d568c681f52d8a237e8862a0cd8c546ba255" +checksum = "79b4bdd5f1c0c7493d780c645f0bff5b9361e6408210fa88910adb181efca64c" dependencies = [ "base64 0.22.1", "base64-serde", - "bytes", "chrono", "fs-err", "iso8601", @@ -561,9 +553,9 @@ dependencies = [ [[package]] name = "either" -version = "1.12.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "encode_unicode" @@ -588,9 +580,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", "windows-sys 0.52.0", @@ -739,15 +731,15 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.4.5" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ - "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", + "futures-util", "http", "indexmap 2.2.6", "slab", @@ -985,9 +977,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.13" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] @@ -1049,9 +1041,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libredox" @@ -1065,9 +1057,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "log" @@ -1105,9 +1097,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -1125,8 +1117,8 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" -source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=c7ac566#c7ac566559d441bbc3e5e5bd04fb7162c38d88b0" +version = "0.2.11" +source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=fdba246#fdba246a79986607cbdf573733445498bb6da2a9" dependencies = [ "libc", "log", @@ -1163,9 +1155,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.6" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" dependencies = [ "num-traits", ] @@ -1259,9 +1251,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.0+3.3.0" +version = "300.2.3+3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba8804a1c5765b18c4b3f907e6897ebabeedebc9830e1a0046c4a4cf44663e1" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" dependencies = [ "cc", ] @@ -1352,9 +1344,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" dependencies = [ "unicode-ident", ] @@ -1519,13 +1511,12 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" [[package]] name = "rsubs-lib" -version = "0.3.2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9f50e3fbcbf1f0bd109954e2dd813d1715c7b4a92a7bf159a85dea49e9d863" +checksum = "9dcca2a9560fca05de8f95bc3767e46673d4b4c1f2c7a11092e10efd95bbdf62" dependencies = [ "regex", "serde", - "time", ] [[package]] @@ -1542,9 +1533,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustfft" @@ -1613,15 +1604,15 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" dependencies = [ "ring", "rustls-pki-types", @@ -1640,9 +1631,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schannel" @@ -1678,18 +1669,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.202" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.202" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", @@ -1698,9 +1689,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ "itoa", "ryu", @@ -1828,6 +1819,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.1" @@ -1842,9 +1839,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.65" +version = "2.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106" +checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" dependencies = [ "proc-macro2", "quote", @@ -1901,18 +1898,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", @@ -1967,9 +1964,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -1984,9 +1981,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c1e28bb..26f71f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "crunchy-cli" authors = ["Crunchy Labs Maintainers"] -version = "3.6.7" +version = "3.6.1" edition = "2021" license = "MIT" @@ -14,9 +14,9 @@ openssl-tls = ["dep:native-tls-crate", "native-tls-crate/openssl", "crunchy-cli- openssl-tls-static = ["dep:native-tls-crate", "native-tls-crate/openssl", "crunchy-cli-core/openssl-tls-static"] [dependencies] -tokio = { version = "1.38", features = ["macros", "rt-multi-thread", "time"], default-features = false } +tokio = { version = "1.37", features = ["macros", "rt-multi-thread", "time"], default-features = false } -native-tls-crate = { package = "native-tls", version = "0.2.12", optional = true } +native-tls-crate = { package = "native-tls", version = "0.2.11", optional = true } crunchy-cli-core = { path = "./crunchy-cli-core" } @@ -34,7 +34,7 @@ members = ["crunchy-cli-core"] [patch.crates-io] # fork of the `native-tls` crate which can use openssl as backend on every platform. this is done as `reqwest` only # supports `rustls` and `native-tls` as tls backend -native-tls = { git = "https://github.com/crunchy-labs/rust-not-so-native-tls.git", rev = "c7ac566" } +native-tls = { git = "https://github.com/crunchy-labs/rust-not-so-native-tls.git", rev = "fdba246" } [profile.release] strip = true diff --git a/README.md b/README.md index 1ae2645..45b8ea7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# This project has been sunset as Crunchyroll moved to a DRM-only system. See [#362](https://github.com/crunchy-labs/crunchy-cli/issues/362). +> ~~This project has been sunset as Crunchyroll moved to a DRM-only system. See [#362](https://github.com/crunchy-labs/crunchy-cli/issues/362).~~ +> +> Well there is one endpoint which still has DRM-free streams, I guess I still have a bit time until (finally) everything is DRM-only. # crunchy-cli diff --git a/crunchy-cli-core/Cargo.toml b/crunchy-cli-core/Cargo.toml index 399053f..8e1ba3a 100644 --- a/crunchy-cli-core/Cargo.toml +++ b/crunchy-cli-core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "crunchy-cli-core" authors = ["Crunchy Labs Maintainers"] -version = "3.6.7" +version = "3.6.1" edition = "2021" license = "MIT" @@ -16,7 +16,7 @@ anyhow = "1.0" async-speed-limit = "0.4" clap = { version = "4.5", features = ["derive", "string"] } chrono = "0.4" -crunchyroll-rs = { version = "0.11.4", features = ["experimental-stabilizations", "tower"] } +crunchyroll-rs = { version = "0.11.1", features = ["experimental-stabilizations", "tower"] } ctrlc = "3.4" dialoguer = { version = "0.11", default-features = false } dirs = "5.0" @@ -30,7 +30,7 @@ log = { version = "0.4", features = ["std"] } num_cpus = "1.16" regex = "1.10" reqwest = { version = "0.12", features = ["socks", "stream"] } -rsubs-lib = "~0.3.2" +rsubs-lib = ">=0.2.1" rusty-chromaprint = "0.2" serde = "1.0" serde_json = "1.0" @@ -38,8 +38,7 @@ serde_plain = "1.0" shlex = "1.3" sys-locale = "0.3" tempfile = "3.10" -time = "0.3" -tokio = { version = "1.38", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] } +tokio = { version = "1.37", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] } tokio-util = "0.7" tower-service = "0.3" rustls-native-certs = { version = "0.7", optional = true } diff --git a/crunchy-cli-core/src/archive/command.rs b/crunchy-cli-core/src/archive/command.rs index 0d1b3a4..38bddc1 100644 --- a/crunchy-cli-core/src/archive/command.rs +++ b/crunchy-cli-core/src/archive/command.rs @@ -1,9 +1,10 @@ +use crate::archive::filter::ArchiveFilter; use crate::utils::context::Context; use crate::utils::download::{ DownloadBuilder, DownloadFormat, DownloadFormatMetadata, MergeBehavior, }; use crate::utils::ffmpeg::FFmpegPreset; -use crate::utils::filter::{Filter, FilterMediaScope}; +use crate::utils::filter::Filter; use crate::utils::format::{Format, SingleFormat}; use crate::utils::locale::{all_locale_in_locales, resolve_locales, LanguageTagging}; use crate::utils::log::progress; @@ -233,10 +234,6 @@ impl Execute for Archive { bail!("`--include-chapters` can only be used if `--merge` is set to 'audio' or 'sync'") } - if !self.skip_existing_method.is_empty() && !self.skip_existing { - warn!("`--skip-existing-method` has no effect if `--skip-existing` is not set") - } - self.audio = all_locale_in_locales(self.audio.clone()); self.subtitle = all_locale_in_locales(self.subtitle.clone()); @@ -283,49 +280,9 @@ impl Execute for Archive { for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() { let progress_handler = progress!("Fetching series details"); - let single_format_collection = Filter::new( + let single_format_collection = ArchiveFilter::new( url_filter, - self.audio.clone(), - self.subtitle.clone(), - |scope, locales| { - let audios = locales.into_iter().map(|l| l.to_string()).collect::>().join(", "); - match scope { - FilterMediaScope::Series(series) => warn!("Series {} is not available with {} audio", series.title, audios), - FilterMediaScope::Season(season) => warn!("Season {} is not available with {} audio", season.season_number, audios), - FilterMediaScope::Episode(episodes) => { - if episodes.len() == 1 { - warn!("Episode {} is not available with {} audio", episodes[0].sequence_number, audios) - } else if episodes.len() == 2 { - warn!("Season {} is only available with {} audio from episode {} to {}", episodes[0].season_number, audios, episodes[0].sequence_number, episodes[1].sequence_number) - } else { - unimplemented!() - } - } - } - Ok(true) - }, - |scope, locales| { - let subtitles = locales.into_iter().map(|l| l.to_string()).collect::>().join(", "); - match scope { - FilterMediaScope::Series(series) => warn!("Series {} is not available with {} subtitles", series.title, subtitles), - FilterMediaScope::Season(season) => warn!("Season {} is not available with {} subtitles", season.season_number, subtitles), - FilterMediaScope::Episode(episodes) => { - if episodes.len() == 1 { - warn!("Episode {} of season {} is not available with {} subtitles", episodes[0].sequence_number, episodes[0].season_title, subtitles) - } else if episodes.len() == 2 { - warn!("Season {} of season {} is only available with {} subtitles from episode {} to {}", episodes[0].season_number, episodes[0].season_title, subtitles, episodes[0].sequence_number, episodes[1].sequence_number) - } else { - unimplemented!() - } - } - } - Ok(true) - }, - |season| { - warn!("Skipping premium episodes in season {season}"); - Ok(()) - }, - Format::has_relative_fmt(&self.output), + self.clone(), !self.yes, self.skip_specials, ctx.crunchy.premium().await, @@ -540,28 +497,29 @@ async fn get_format( .subtitle .iter() .flat_map(|s| { - let mut subtitles = vec![]; - if let Some(caption) = stream.captions.get(s) { - subtitles.push((caption.clone(), true)) - } - if let Some(subtitle) = stream.subtitles.get(s) { - // the subtitle is probably cc if the audio is not japanese or only one subtitle - // exists for this stream - let cc = single_format.audio != Locale::ja_JP && stream.subtitles.len() == 1; - // only include the subtitles if no cc subtitle is already present or if it's - // not cc - if subtitles.is_empty() || !cc { - subtitles.push((subtitle.clone(), cc)) - } - } + let subtitles = stream + .subtitles + .get(s) + .cloned() + // the subtitle is probably cc if the audio is not japanese or only one + // subtitle exists for this stream + .map(|l| { + ( + l, + single_format.audio != Locale::ja_JP && stream.subtitles.len() == 1, + ) + }); + let cc = stream.captions.get(s).cloned().map(|l| (l, true)); + subtitles + .into_iter() + .chain(cc.into_iter()) + .collect::>() }) .collect(); format_pairs.push((single_format, video.clone(), audio, subtitles.clone())); - single_format_to_format_pairs.push((single_format.clone(), video, subtitles)); - - stream.invalidate().await? + single_format_to_format_pairs.push((single_format.clone(), video, subtitles)) } let mut download_formats = vec![]; diff --git a/crunchy-cli-core/src/archive/filter.rs b/crunchy-cli-core/src/archive/filter.rs new file mode 100644 index 0000000..b08fb6c --- /dev/null +++ b/crunchy-cli-core/src/archive/filter.rs @@ -0,0 +1,466 @@ +use crate::archive::command::Archive; +use crate::utils::filter::{real_dedup_vec, Filter}; +use crate::utils::format::{Format, SingleFormat, SingleFormatCollection}; +use crate::utils::interactive_select::{check_for_duplicated_seasons, get_duplicated_seasons}; +use crate::utils::parse::{fract, UrlFilter}; +use anyhow::Result; +use crunchyroll_rs::{Concert, Episode, Locale, Movie, MovieListing, MusicVideo, Season, Series}; +use log::{info, warn}; +use std::collections::{BTreeMap, HashMap}; +use std::ops::Not; + +enum Visited { + Series, + Season, + None, +} + +pub(crate) struct ArchiveFilter { + url_filter: UrlFilter, + archive: Archive, + interactive_input: bool, + skip_special: bool, + season_episodes: HashMap>, + season_subtitles_missing: Vec, + seasons_with_premium: Option>, + season_sorting: Vec, + visited: Visited, +} + +impl ArchiveFilter { + pub(crate) fn new( + url_filter: UrlFilter, + archive: Archive, + interactive_input: bool, + skip_special: bool, + is_premium: bool, + ) -> Self { + Self { + url_filter, + archive, + interactive_input, + skip_special, + season_episodes: HashMap::new(), + season_subtitles_missing: vec![], + seasons_with_premium: is_premium.not().then_some(vec![]), + season_sorting: vec![], + visited: Visited::None, + } + } +} + +impl Filter for ArchiveFilter { + type T = Vec; + type Output = SingleFormatCollection; + + async fn visit_series(&mut self, series: Series) -> Result> { + // `series.audio_locales` isn't always populated b/c of crunchyrolls api. so check if the + // audio is matching only if the field is populated + if !series.audio_locales.is_empty() { + let missing_audio = missing_locales(&series.audio_locales, &self.archive.audio); + if !missing_audio.is_empty() { + warn!( + "Series {} is not available with {} audio", + series.title, + missing_audio + .into_iter() + .map(|l| l.to_string()) + .collect::>() + .join(", ") + ) + } + let missing_subtitle = + missing_locales(&series.subtitle_locales, &self.archive.subtitle); + if !missing_subtitle.is_empty() { + warn!( + "Series {} is not available with {} subtitles", + series.title, + missing_subtitle + .into_iter() + .map(|l| l.to_string()) + .collect::>() + .join(", ") + ) + } + self.visited = Visited::Series + } + + let mut seasons = series.seasons().await?; + let mut remove_ids = vec![]; + for season in seasons.iter_mut() { + if !self.url_filter.is_season_valid(season.season_number) + || (!season + .audio_locales + .iter() + .any(|l| self.archive.audio.contains(l)) + && !season + .available_versions() + .await? + .iter() + .any(|l| self.archive.audio.contains(l))) + { + remove_ids.push(season.id.clone()); + } + } + + seasons.retain(|s| !remove_ids.contains(&s.id)); + + let duplicated_seasons = get_duplicated_seasons(&seasons); + if !duplicated_seasons.is_empty() { + if self.interactive_input { + check_for_duplicated_seasons(&mut seasons); + } else { + info!( + "Found duplicated seasons: {}", + duplicated_seasons + .iter() + .map(|d| d.to_string()) + .collect::>() + .join(", ") + ) + } + } + + Ok(seasons) + } + + async fn visit_season(&mut self, mut season: Season) -> Result> { + if !self.url_filter.is_season_valid(season.season_number) { + return Ok(vec![]); + } + + let mut seasons = season.version(self.archive.audio.clone()).await?; + if self + .archive + .audio + .iter() + .any(|l| season.audio_locales.contains(l)) + { + seasons.insert(0, season.clone()); + } + + if !matches!(self.visited, Visited::Series) { + let mut audio_locales: Vec = seasons + .iter() + .flat_map(|s| s.audio_locales.clone()) + .collect(); + real_dedup_vec(&mut audio_locales); + let missing_audio = missing_locales(&audio_locales, &self.archive.audio); + if !missing_audio.is_empty() { + warn!( + "Season {} is not available with {} audio", + season.season_number, + missing_audio + .into_iter() + .map(|l| l.to_string()) + .collect::>() + .join(", ") + ) + } + + let subtitle_locales: Vec = seasons + .iter() + .flat_map(|s| s.subtitle_locales.clone()) + .collect(); + let missing_subtitle = missing_locales(&subtitle_locales, &self.archive.subtitle); + if !missing_subtitle.is_empty() { + warn!( + "Season {} is not available with {} subtitles", + season.season_number, + missing_subtitle + .into_iter() + .map(|l| l.to_string()) + .collect::>() + .join(", ") + ) + } + self.visited = Visited::Season + } + + let mut episodes = vec![]; + for season in seasons { + self.season_sorting.push(season.id.clone()); + let season_locale = if season.audio_locales.len() < 2 { + Some( + season + .audio_locales + .first() + .cloned() + .unwrap_or(Locale::ja_JP), + ) + } else { + None + }; + let mut eps = season.episodes().await?; + let before_len = eps.len(); + + for mut ep in eps.clone() { + if let Some(l) = &season_locale { + if &ep.audio_locale == l { + continue; + } + eps.remove(eps.iter().position(|p| p.id == ep.id).unwrap()); + } else { + let mut requested_locales = self.archive.audio.clone(); + if let Some(idx) = requested_locales.iter().position(|p| p == &ep.audio_locale) + { + requested_locales.remove(idx); + } else { + eps.remove(eps.iter().position(|p| p.id == ep.id).unwrap()); + } + eps.extend(ep.version(self.archive.audio.clone()).await?); + } + } + if eps.len() < before_len { + if eps.is_empty() { + if matches!(self.visited, Visited::Series) { + warn!( + "Season {} is not available with {} audio", + season.season_number, + season_locale.unwrap_or(Locale::ja_JP) + ) + } + } else { + let last_episode = eps.last().unwrap(); + warn!( + "Season {} is only available with {} audio until episode {} ({})", + season.season_number, + season_locale.unwrap_or(Locale::ja_JP), + last_episode.sequence_number, + last_episode.title + ) + } + } + episodes.extend(eps) + } + + if Format::has_relative_fmt(&self.archive.output) { + for episode in episodes.iter() { + self.season_episodes + .entry(episode.season_id.clone()) + .or_default() + .push(episode.clone()) + } + } + + Ok(episodes) + } + + async fn visit_episode(&mut self, mut episode: Episode) -> Result> { + if !self + .url_filter + .is_episode_valid(episode.sequence_number, episode.season_number) + { + return Ok(None); + } + + // skip the episode if it's a special + if self.skip_special + && (episode.sequence_number == 0.0 || episode.sequence_number.fract() != 0.0) + { + return Ok(None); + } + + let mut episodes = vec![]; + if !matches!(self.visited, Visited::Series) && !matches!(self.visited, Visited::Season) { + if self.archive.audio.contains(&episode.audio_locale) { + episodes.push((episode.clone(), episode.subtitle_locales.clone())) + } + episodes.extend( + episode + .version(self.archive.audio.clone()) + .await? + .into_iter() + .map(|e| (e.clone(), e.subtitle_locales.clone())), + ); + let audio_locales: Vec = episodes + .iter() + .map(|(e, _)| e.audio_locale.clone()) + .collect(); + let missing_audio = missing_locales(&audio_locales, &self.archive.audio); + if !missing_audio.is_empty() { + warn!( + "Episode {} is not available with {} audio", + episode.sequence_number, + missing_audio + .into_iter() + .map(|l| l.to_string()) + .collect::>() + .join(", ") + ) + } + + let mut subtitle_locales: Vec = + episodes.iter().flat_map(|(_, s)| s.clone()).collect(); + real_dedup_vec(&mut subtitle_locales); + let missing_subtitles = missing_locales(&subtitle_locales, &self.archive.subtitle); + if !missing_subtitles.is_empty() + && !self + .season_subtitles_missing + .contains(&episode.season_number) + { + warn!( + "Episode {} is not available with {} subtitles", + episode.sequence_number, + missing_subtitles + .into_iter() + .map(|l| l.to_string()) + .collect::>() + .join(", ") + ); + self.season_subtitles_missing.push(episode.season_number) + } + } else { + episodes.push((episode.clone(), episode.subtitle_locales.clone())) + } + + if self.seasons_with_premium.is_some() { + let episode_len_before = episodes.len(); + episodes.retain(|(e, _)| !e.is_premium_only); + if episode_len_before < episodes.len() + && !self + .seasons_with_premium + .as_ref() + .unwrap() + .contains(&episode.season_number) + { + warn!( + "Skipping premium episodes in season {}", + episode.season_number + ); + self.seasons_with_premium + .as_mut() + .unwrap() + .push(episode.season_number) + } + + if episodes.is_empty() { + return Ok(None); + } + } + + let mut relative_episode_number = None; + let mut relative_sequence_number = None; + // get the relative episode number. only done if the output string has the pattern to include + // the relative episode number as this requires some extra fetching + if Format::has_relative_fmt(&self.archive.output) { + let season_eps = match self.season_episodes.get(&episode.season_id) { + Some(eps) => eps, + None => { + self.season_episodes.insert( + episode.season_id.clone(), + episode.season().await?.episodes().await?, + ); + self.season_episodes.get(&episode.season_id).unwrap() + } + }; + let mut non_integer_sequence_number_count = 0; + for (i, ep) in season_eps.iter().enumerate() { + if ep.sequence_number.fract() != 0.0 || ep.sequence_number == 0.0 { + non_integer_sequence_number_count += 1; + } + if ep.id == episode.id { + relative_episode_number = Some(i + 1); + relative_sequence_number = Some( + (i + 1 - non_integer_sequence_number_count) as f32 + + fract(ep.sequence_number), + ); + break; + } + } + if relative_episode_number.is_none() || relative_sequence_number.is_none() { + warn!( + "Failed to get relative episode number for episode {} ({}) of {} season {}", + episode.sequence_number, + episode.title, + episode.series_title, + episode.season_number, + ) + } + } + + Ok(Some( + episodes + .into_iter() + .map(|(e, s)| { + SingleFormat::new_from_episode( + e, + s, + relative_episode_number.map(|n| n as u32), + relative_sequence_number, + ) + }) + .collect(), + )) + } + + async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result> { + Ok(movie_listing.movies().await?) + } + + async fn visit_movie(&mut self, movie: Movie) -> Result> { + Ok(Some(vec![SingleFormat::new_from_movie(movie, vec![])])) + } + + async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result> { + Ok(Some(vec![SingleFormat::new_from_music_video(music_video)])) + } + + async fn visit_concert(&mut self, concert: Concert) -> Result> { + Ok(Some(vec![SingleFormat::new_from_concert(concert)])) + } + + async fn finish(self, input: Vec) -> Result { + let flatten_input: Self::T = input.into_iter().flatten().collect(); + + let mut single_format_collection = SingleFormatCollection::new(); + + let mut pre_sorted: BTreeMap = BTreeMap::new(); + for data in flatten_input { + pre_sorted + .entry(data.identifier.clone()) + .or_insert(vec![]) + .push(data) + } + + let mut sorted: Vec<(String, Self::T)> = pre_sorted.into_iter().collect(); + sorted.sort_by(|(_, a), (_, b)| { + self.season_sorting + .iter() + .position(|p| p == &a.first().unwrap().season_id) + .unwrap() + .cmp( + &self + .season_sorting + .iter() + .position(|p| p == &b.first().unwrap().season_id) + .unwrap(), + ) + }); + + for (_, mut data) in sorted { + data.sort_by(|a, b| { + self.archive + .audio + .iter() + .position(|p| p == &a.audio) + .unwrap_or(usize::MAX) + .cmp( + &self + .archive + .audio + .iter() + .position(|p| p == &b.audio) + .unwrap_or(usize::MAX), + ) + }); + single_format_collection.add_single_formats(data) + } + + Ok(single_format_collection) + } +} + +fn missing_locales<'a>(available: &[Locale], searched: &'a [Locale]) -> Vec<&'a Locale> { + searched.iter().filter(|p| !available.contains(p)).collect() +} diff --git a/crunchy-cli-core/src/archive/mod.rs b/crunchy-cli-core/src/archive/mod.rs index 670d0c2..c3544a4 100644 --- a/crunchy-cli-core/src/archive/mod.rs +++ b/crunchy-cli-core/src/archive/mod.rs @@ -1,3 +1,4 @@ mod command; +mod filter; pub use command::Archive; diff --git a/crunchy-cli-core/src/download/command.rs b/crunchy-cli-core/src/download/command.rs index 8e3794f..bb0c1fd 100644 --- a/crunchy-cli-core/src/download/command.rs +++ b/crunchy-cli-core/src/download/command.rs @@ -1,7 +1,8 @@ +use crate::download::filter::DownloadFilter; use crate::utils::context::Context; use crate::utils::download::{DownloadBuilder, DownloadFormat, DownloadFormatMetadata}; use crate::utils::ffmpeg::{FFmpegPreset, SOFTSUB_CONTAINERS}; -use crate::utils::filter::{Filter, FilterMediaScope}; +use crate::utils::filter::Filter; use crate::utils::format::{Format, SingleFormat}; use crate::utils::locale::{resolve_locales, LanguageTagging}; use crate::utils::log::progress; @@ -13,7 +14,7 @@ use anyhow::bail; use anyhow::Result; use crunchyroll_rs::media::Resolution; use crunchyroll_rs::Locale; -use log::{debug, error, warn}; +use log::{debug, warn}; use std::collections::HashMap; use std::path::Path; @@ -249,53 +250,9 @@ impl Execute for Download { for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() { let progress_handler = progress!("Fetching series details"); - let single_format_collection = Filter::new( + let single_format_collection = DownloadFilter::new( url_filter, - vec![self.audio.clone()], - self.subtitle.as_ref().map_or(vec![], |s| vec![s.clone()]), - |scope, locales| { - match scope { - FilterMediaScope::Series(series) => bail!("Series {} is not available with {} audio", series.title, locales[0]), - FilterMediaScope::Season(season) => { - error!("Season {} is not available with {} audio", season.season_number, locales[0]); - Ok(false) - } - FilterMediaScope::Episode(episodes) => { - if episodes.len() == 1 { - warn!("Episode {} of season {} is not available with {} audio", episodes[0].sequence_number, episodes[0].season_title, locales[0]) - } else if episodes.len() == 2 { - warn!("Season {} is only available with {} audio from episode {} to {}", episodes[0].season_number, locales[0], episodes[0].sequence_number, episodes[1].sequence_number) - } else { - unimplemented!() - } - Ok(false) - } - } - }, - |scope, locales| { - match scope { - FilterMediaScope::Series(series) => bail!("Series {} is not available with {} subtitles", series.title, locales[0]), - FilterMediaScope::Season(season) => { - warn!("Season {} is not available with {} subtitles", season.season_number, locales[0]); - Ok(false) - }, - FilterMediaScope::Episode(episodes) => { - if episodes.len() == 1 { - warn!("Episode {} of season {} is not available with {} subtitles", episodes[0].sequence_number, episodes[0].season_title, locales[0]) - } else if episodes.len() == 2 { - warn!("Season {} is only available with {} subtitles from episode {} to {}", episodes[0].season_number, locales[0], episodes[0].sequence_number, episodes[1].sequence_number) - } else { - unimplemented!() - } - Ok(false) - } - } - }, - |season| { - warn!("Skipping premium episodes in season {season}"); - Ok(()) - }, - Format::has_relative_fmt(&self.output), + self.clone(), !self.yes, self.skip_specials, ctx.crunchy.premium().await, @@ -427,20 +384,12 @@ async fn get_format( let subtitle = if contains_hardsub { None } else if let Some(subtitle_locale) = &download.subtitle { - if download.audio == Locale::ja_JP { - stream - .subtitles - .get(subtitle_locale) - // use closed captions as fallback if no actual subtitles are found - .or_else(|| stream.captions.get(subtitle_locale)) - .cloned() - } else { - stream - .captions - .get(subtitle_locale) - .or_else(|| stream.subtitles.get(subtitle_locale)) - .cloned() - } + stream + .subtitles + .get(subtitle_locale) + .cloned() + // use closed captions as fallback if no actual subtitles are found + .or_else(|| stream.captions.get(subtitle_locale).cloned()) } else { None }; @@ -477,7 +426,5 @@ async fn get_format( subs.push(download.subtitle.clone().unwrap()) } - stream.invalidate().await?; - Ok((download_format, format)) } diff --git a/crunchy-cli-core/src/download/filter.rs b/crunchy-cli-core/src/download/filter.rs new file mode 100644 index 0000000..1c62920 --- /dev/null +++ b/crunchy-cli-core/src/download/filter.rs @@ -0,0 +1,307 @@ +use crate::download::Download; +use crate::utils::filter::Filter; +use crate::utils::format::{Format, SingleFormat, SingleFormatCollection}; +use crate::utils::interactive_select::{check_for_duplicated_seasons, get_duplicated_seasons}; +use crate::utils::parse::{fract, UrlFilter}; +use anyhow::{bail, Result}; +use crunchyroll_rs::{Concert, Episode, Movie, MovieListing, MusicVideo, Season, Series}; +use log::{error, info, warn}; +use std::collections::HashMap; +use std::ops::Not; + +pub(crate) struct DownloadFilter { + url_filter: UrlFilter, + download: Download, + interactive_input: bool, + skip_special: bool, + season_episodes: HashMap>, + season_subtitles_missing: Vec, + seasons_with_premium: Option>, + season_visited: bool, +} + +impl DownloadFilter { + pub(crate) fn new( + url_filter: UrlFilter, + download: Download, + interactive_input: bool, + skip_special: bool, + is_premium: bool, + ) -> Self { + Self { + url_filter, + download, + interactive_input, + skip_special, + season_episodes: HashMap::new(), + season_subtitles_missing: vec![], + seasons_with_premium: is_premium.not().then_some(vec![]), + season_visited: false, + } + } +} + +impl Filter for DownloadFilter { + type T = SingleFormat; + type Output = SingleFormatCollection; + + async fn visit_series(&mut self, series: Series) -> Result> { + // `series.audio_locales` isn't always populated b/c of crunchyrolls api. so check if the + // audio is matching only if the field is populated + if !series.audio_locales.is_empty() && !series.audio_locales.contains(&self.download.audio) + { + error!( + "Series {} is not available with {} audio", + series.title, self.download.audio + ); + return Ok(vec![]); + } + + let mut seasons = vec![]; + for mut season in series.seasons().await? { + if !self.url_filter.is_season_valid(season.season_number) { + continue; + } + + if !season + .audio_locales + .iter() + .any(|l| l == &self.download.audio) + { + if season + .available_versions() + .await? + .iter() + .any(|l| l == &self.download.audio) + { + season = season + .version(vec![self.download.audio.clone()]) + .await? + .remove(0) + } else { + error!( + "Season {} - '{}' is not available with {} audio", + season.season_number, + season.title, + self.download.audio.clone(), + ); + continue; + } + } + + seasons.push(season) + } + + let duplicated_seasons = get_duplicated_seasons(&seasons); + if !duplicated_seasons.is_empty() { + if self.interactive_input { + check_for_duplicated_seasons(&mut seasons); + } else { + info!( + "Found duplicated seasons: {}", + duplicated_seasons + .iter() + .map(|d| d.to_string()) + .collect::>() + .join(", ") + ) + } + } + + Ok(seasons) + } + + async fn visit_season(&mut self, season: Season) -> Result> { + self.season_visited = true; + + let mut episodes = season.episodes().await?; + + if Format::has_relative_fmt(&self.download.output) { + for episode in episodes.iter() { + self.season_episodes + .entry(episode.season_number) + .or_default() + .push(episode.clone()) + } + } + + episodes.retain(|e| { + self.url_filter + .is_episode_valid(e.sequence_number, season.season_number) + }); + + Ok(episodes) + } + + async fn visit_episode(&mut self, mut episode: Episode) -> Result> { + if !self + .url_filter + .is_episode_valid(episode.sequence_number, episode.season_number) + { + return Ok(None); + } + + // skip the episode if it's a special + if self.skip_special + && (episode.sequence_number == 0.0 || episode.sequence_number.fract() != 0.0) + { + return Ok(None); + } + + // check if the audio locale is correct. + // should only be incorrect if the console input was a episode url. otherwise + // `DownloadFilter::visit_season` returns the correct episodes with matching audio + if episode.audio_locale != self.download.audio { + // check if any other version (same episode, other language) of this episode is available + // with the requested audio. if not, return an error + if !episode + .available_versions() + .await? + .contains(&self.download.audio) + { + let error_message = format!( + "Episode {} ({}) of {} season {} is not available with {} audio", + episode.sequence_number, + episode.title, + episode.series_title, + episode.season_number, + self.download.audio + ); + // sometimes a series randomly has episode in an other language. if this is the case, + // only error if the input url was a episode url + if self.season_visited { + warn!("{}", error_message); + return Ok(None); + } else { + bail!("{}", error_message) + } + } + // overwrite the current episode with the other version episode + episode = episode + .version(vec![self.download.audio.clone()]) + .await? + .remove(0) + } + + // check if the subtitles are supported + if let Some(subtitle_locale) = &self.download.subtitle { + if !episode.subtitle_locales.contains(subtitle_locale) { + // if the episode doesn't have the requested subtitles, print a error. to print this + // error only once per season, it's checked if an error got printed before by looking + // up if the season id is present in `self.season_subtitles_missing`. if not, print + // the error and add the season id to `self.season_subtitles_missing`. if it is + // present, skip the error printing + if !self + .season_subtitles_missing + .contains(&episode.season_number) + { + self.season_subtitles_missing.push(episode.season_number); + error!( + "{} season {} is not available with {} subtitles", + episode.series_title, episode.season_number, subtitle_locale + ); + } + return Ok(None); + } + } + + if self.seasons_with_premium.is_some() && episode.is_premium_only { + if !self + .seasons_with_premium + .as_ref() + .unwrap() + .contains(&episode.season_number) + { + warn!( + "Skipping premium episodes in season {}", + episode.season_number + ); + self.seasons_with_premium + .as_mut() + .unwrap() + .push(episode.season_number) + } + + return Ok(None); + } + + let mut relative_episode_number = None; + let mut relative_sequence_number = None; + // get the relative episode number. only done if the output string has the pattern to include + // the relative episode number as this requires some extra fetching + if Format::has_relative_fmt(&self.download.output) { + let season_eps = match self.season_episodes.get(&episode.season_number) { + Some(eps) => eps, + None => { + self.season_episodes.insert( + episode.season_number, + episode.season().await?.episodes().await?, + ); + self.season_episodes.get(&episode.season_number).unwrap() + } + }; + let mut non_integer_sequence_number_count = 0; + for (i, ep) in season_eps.iter().enumerate() { + if ep.sequence_number.fract() != 0.0 || ep.sequence_number == 0.0 { + non_integer_sequence_number_count += 1; + } + if ep.id == episode.id { + relative_episode_number = Some(i + 1); + relative_sequence_number = Some( + (i + 1 - non_integer_sequence_number_count) as f32 + + fract(ep.sequence_number), + ); + break; + } + } + if relative_episode_number.is_none() || relative_sequence_number.is_none() { + warn!( + "Failed to get relative episode number for episode {} ({}) of {} season {}", + episode.sequence_number, + episode.title, + episode.series_title, + episode.season_number, + ) + } + } + + Ok(Some(SingleFormat::new_from_episode( + episode.clone(), + self.download.subtitle.clone().map_or(vec![], |s| { + if episode.subtitle_locales.contains(&s) { + vec![s] + } else { + vec![] + } + }), + relative_episode_number.map(|n| n as u32), + relative_sequence_number, + ))) + } + + async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result> { + Ok(movie_listing.movies().await?) + } + + async fn visit_movie(&mut self, movie: Movie) -> Result> { + Ok(Some(SingleFormat::new_from_movie(movie, vec![]))) + } + + async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result> { + Ok(Some(SingleFormat::new_from_music_video(music_video))) + } + + async fn visit_concert(&mut self, concert: Concert) -> Result> { + Ok(Some(SingleFormat::new_from_concert(concert))) + } + + async fn finish(self, input: Vec) -> Result { + let mut single_format_collection = SingleFormatCollection::new(); + + for data in input { + single_format_collection.add_single_formats(vec![data]) + } + + Ok(single_format_collection) + } +} diff --git a/crunchy-cli-core/src/download/mod.rs b/crunchy-cli-core/src/download/mod.rs index 47ca304..696872e 100644 --- a/crunchy-cli-core/src/download/mod.rs +++ b/crunchy-cli-core/src/download/mod.rs @@ -1,3 +1,4 @@ mod command; +mod filter; pub use command::Download; diff --git a/crunchy-cli-core/src/search/command.rs b/crunchy-cli-core/src/search/command.rs index 8032bed..c29ce34 100644 --- a/crunchy-cli-core/src/search/command.rs +++ b/crunchy-cli-core/src/search/command.rs @@ -111,10 +111,6 @@ impl Execute for Search { warn!("Using `search` anonymously or with a non-premium account may return incomplete results") } - if self.output.contains("{{stream.is_drm}}") { - warn!("The `{{{{stream.is_drm}}}}` option is deprecated as it isn't reliable anymore and will be removed soon") - } - let input = if crunchyroll_rs::parse::parse_url(&self.input).is_some() { match parse_url(&ctx.crunchy, self.input.clone(), true).await { Ok(ok) => vec![ok], diff --git a/crunchy-cli-core/src/search/format.rs b/crunchy-cli-core/src/search/format.rs index cf3c5bc..7ea84d8 100644 --- a/crunchy-cli-core/src/search/format.rs +++ b/crunchy-cli-core/src/search/format.rs @@ -173,7 +173,7 @@ impl From<&Stream> for FormatStream { Self { locale: value.audio_locale.clone(), dash_url: value.url.clone(), - is_drm: false, + is_drm: value.session.uses_stream_limits, } } } @@ -241,6 +241,14 @@ macro_rules! must_match_if_true { }; } +macro_rules! self_and_versions { + ($var:expr => $audio:expr) => {{ + let mut items = vec![$var.clone()]; + items.extend($var.clone().version($audio).await?); + items + }}; +} + pub struct Format { pattern: Vec<(Range, Scope, String)>, pattern_count: HashMap, @@ -413,15 +421,7 @@ impl Format { }; let mut seasons = vec![]; for season in tmp_seasons { - seasons.push(season.clone()); - for version in season.versions { - if season.id == version.id { - continue; - } - if self.filter_options.audio.contains(&version.audio_locale) { - seasons.push(version.season().await?) - } - } + seasons.extend(self_and_versions!(season => self.filter_options.audio.clone())) } tree.extend( self.filter_options @@ -435,15 +435,7 @@ impl Format { if !episode_empty || !stream_empty { match &media_collection { MediaCollection::Episode(episode) => { - let mut episodes = vec![episode.clone()]; - for version in &episode.versions { - if episode.id == version.id { - continue; - } - if self.filter_options.audio.contains(&version.audio_locale) { - episodes.push(version.episode().await?) - } - } + let episodes = self_and_versions!(episode => self.filter_options.audio.clone()); tree.push(( Season::default(), episodes @@ -472,9 +464,7 @@ impl Format { if !stream_empty { for (_, episodes) in tree.iter_mut() { for (episode, streams) in episodes { - let stream = episode.stream_maybe_without_drm().await?; - stream.clone().invalidate().await?; - streams.push(stream) + streams.push(episode.stream_maybe_without_drm().await?) } } } else { diff --git a/crunchy-cli-core/src/utils/download.rs b/crunchy-cli-core/src/utils/download.rs index 2e8f321..565ed7d 100644 --- a/crunchy-cli-core/src/utils/download.rs +++ b/crunchy-cli-core/src/utils/download.rs @@ -13,19 +13,17 @@ use indicatif::{ProgressBar, ProgressDrawTarget, ProgressFinish, ProgressStyle}; use log::{debug, warn, LevelFilter}; use regex::Regex; use reqwest::Client; -use rsubs_lib::{SSA, VTT}; +use rsubs_lib::{ssa, vtt}; use std::borrow::Borrow; use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap}; use std::io::Write; -use std::ops::Add; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::Arc; use std::time::Duration; use std::{env, fs}; use tempfile::TempPath; -use time::Time; use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader}; use tokio::select; use tokio::sync::mpsc::unbounded_channel; @@ -322,25 +320,35 @@ impl Downloader { if let Some(offsets) = offsets { let mut root_format_idx = 0; - let mut root_format_offset = u64::MAX; - + let mut root_format_length = 0; + let mut audio_count: usize = 0; + let mut subtitle_count: usize = 0; for (i, format) in self.formats.iter().enumerate() { let offset = offsets.get(&i).copied().unwrap_or_default(); - let format_offset = offset.num_milliseconds() as u64; - if format_offset < root_format_offset { + let format_len = format + .video + .0 + .segments() + .iter() + .map(|s| s.length.as_millis()) + .sum::() as u64 + - offset.num_milliseconds() as u64; + if format_len > root_format_length { root_format_idx = i; - root_format_offset = format_offset; + root_format_length = format_len; } for _ in &format.audios { if let Some(offset) = &offsets.get(&i) { - audio_offsets.insert(i, **offset); + audio_offsets.insert(audio_count, **offset); } + audio_count += 1 } for _ in &format.subtitles { if let Some(offset) = &offsets.get(&i) { - subtitle_offsets.insert(i, **offset); + subtitle_offsets.insert(subtitle_count, **offset); } + subtitle_count += 1 } } @@ -561,7 +569,7 @@ impl Downloader { for (i, meta) in videos.iter().enumerate() { if let Some(start_time) = meta.start_time { - input.extend(["-itsoffset".to_string(), format_time_delta(&start_time)]) + input.extend(["-ss".to_string(), format_time_delta(&start_time)]) } input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]); maps.extend(["-map".to_string(), i.to_string()]); @@ -582,7 +590,7 @@ impl Downloader { } for (i, meta) in audios.iter().enumerate() { if let Some(start_time) = meta.start_time { - input.extend(["-itsoffset".to_string(), format_time_delta(&start_time)]) + input.extend(["-ss".to_string(), format_time_delta(&start_time)]) } input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]); maps.extend(["-map".to_string(), (i + videos.len()).to_string()]); @@ -629,7 +637,7 @@ impl Downloader { if container_supports_softsubs { for (i, meta) in subtitles.iter().enumerate() { if let Some(start_time) = meta.start_time { - input.extend(["-itsoffset".to_string(), format_time_delta(&start_time)]) + input.extend(["-ss".to_string(), format_time_delta(&start_time)]) } input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]); maps.extend([ @@ -648,7 +656,7 @@ impl Downloader { metadata.extend([ format!("-metadata:s:s:{}", i), format!("title={}", { - let mut title = meta.locale.to_human_readable(); + let mut title = meta.locale.to_string(); if meta.cc { title += " (CC)" } @@ -925,43 +933,36 @@ impl Downloader { ) -> Result { let buf = subtitle.data().await?; let mut ass = match subtitle.format.as_str() { - "ass" => SSA::parse(String::from_utf8_lossy(&buf))?, - "vtt" => VTT::parse(String::from_utf8_lossy(&buf))?.to_ssa(), + "ass" => ssa::parse(String::from_utf8_lossy(&buf).to_string()), + "vtt" => vtt::parse(String::from_utf8_lossy(&buf).to_string()).to_ass(), _ => bail!("unknown subtitle format: {}", subtitle.format), }; // subtitles aren't always correct sorted and video players may have issues with that. to // prevent issues, the subtitles are sorted - // (https://github.com/crunchy-labs/crunchy-cli/issues/208) - ass.events.sort_by(|a, b| a.start.cmp(&b.start)); + ass.events + .sort_by(|a, b| a.line_start.total_ms().cmp(&b.line_start.total_ms())); // it might be the case that the start and/or end time are greater than the actual video // length. this might also result in issues with video players, thus the times are stripped - // to be at most as long as `max_length` - // (https://github.com/crunchy-labs/crunchy-cli/issues/32) + // to be maxim for i in (0..ass.events.len()).rev() { - let max_len = Time::from_hms(0, 0, 0) - .unwrap() - .add(Duration::from_millis(max_length.num_milliseconds() as u64)); - - if ass.events[i].start > max_len { - if ass.events[i].end > max_len { - ass.events[i].start = max_len + if ass.events[i].line_end.total_ms() > max_length.num_milliseconds() as u32 { + if ass.events[i].line_start.total_ms() > max_length.num_milliseconds() as u32 { + ass.events[i] + .line_start + .set_ms(max_length.num_milliseconds() as u32); } - ass.events[i].end = max_len + ass.events[i] + .line_end + .set_ms(max_length.num_milliseconds() as u32); } else { break; } } - // without this additional info, subtitle look very messy in some video player - // (https://github.com/crunchy-labs/crunchy-cli/issues/66) - ass.info - .additional_fields - .insert("ScaledBorderAndShadow".to_string(), "yes".to_string()); - let tempfile = tempfile(".ass")?; let path = tempfile.into_temp_path(); - fs::write(&path, ass.to_string())?; + ass.to_file(path.to_string_lossy().to_string().as_str())?; Ok(path) } diff --git a/crunchy-cli-core/src/utils/filter.rs b/crunchy-cli-core/src/utils/filter.rs index 3388741..63fac9d 100644 --- a/crunchy-cli-core/src/utils/filter.rs +++ b/crunchy-cli-core/src/utils/filter.rs @@ -1,407 +1,24 @@ -use crate::utils::format::{SingleFormat, SingleFormatCollection}; -use crate::utils::interactive_select::{check_for_duplicated_seasons, get_duplicated_seasons}; -use crate::utils::parse::{fract, UrlFilter}; use anyhow::Result; use crunchyroll_rs::{ - Concert, Episode, Locale, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series, + Concert, Episode, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series, }; -use log::{info, warn}; -use std::collections::{BTreeMap, HashMap}; -use std::ops::Not; -pub(crate) enum FilterMediaScope<'a> { - Series(&'a Series), - Season(&'a Season), - /// Always contains 1 or 2 episodes. - /// - 1: The episode's audio is completely missing - /// - 2: The requested audio is only available from first entry to last entry - Episode(Vec<&'a Episode>), -} +pub trait Filter { + type T: Send + Sized; + type Output: Send + Sized; -pub(crate) struct Filter { - url_filter: UrlFilter, + async fn visit_series(&mut self, series: Series) -> Result>; + async fn visit_season(&mut self, season: Season) -> Result>; + async fn visit_episode(&mut self, episode: Episode) -> Result>; + async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result>; + async fn visit_movie(&mut self, movie: Movie) -> Result>; + async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result>; + async fn visit_concert(&mut self, concert: Concert) -> Result>; - skip_specials: bool, - interactive_input: bool, - - relative_episode_number: bool, - - audio_locales: Vec, - subtitle_locales: Vec, - - audios_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result, - subtitles_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result, - no_premium: fn(u32) -> Result<()>, - - is_premium: bool, - - series_visited: bool, - season_episodes: HashMap>, - season_with_premium: Option>, - season_sorting: Vec, -} - -impl Filter { - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - url_filter: UrlFilter, - audio_locales: Vec, - subtitle_locales: Vec, - audios_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result, - subtitles_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result, - no_premium: fn(u32) -> Result<()>, - relative_episode_number: bool, - interactive_input: bool, - skip_specials: bool, - is_premium: bool, - ) -> Self { - Self { - url_filter, - audio_locales, - subtitle_locales, - relative_episode_number, - interactive_input, - audios_missing, - subtitles_missing, - no_premium, - is_premium, - series_visited: false, - season_episodes: HashMap::new(), - skip_specials, - season_with_premium: is_premium.not().then_some(vec![]), - season_sorting: vec![], - } - } - - async fn visit_series(&mut self, series: Series) -> Result> { - // the audio locales field isn't always populated - if !series.audio_locales.is_empty() { - let missing_audios = missing_locales(&series.audio_locales, &self.audio_locales); - if !missing_audios.is_empty() - && !(self.audios_missing)(FilterMediaScope::Series(&series), missing_audios)? - { - return Ok(vec![]); - } - let missing_subtitles = - missing_locales(&series.subtitle_locales, &self.subtitle_locales); - if !missing_subtitles.is_empty() - && !(self.subtitles_missing)(FilterMediaScope::Series(&series), missing_subtitles)? - { - return Ok(vec![]); - } - } - - let mut seasons = vec![]; - for season in series.seasons().await? { - if !self.url_filter.is_season_valid(season.season_number) { - continue; - } - let missing_audios = missing_locales( - &season - .versions - .iter() - .map(|l| l.audio_locale.clone()) - .collect::>(), - &self.audio_locales, - ); - if !missing_audios.is_empty() - && !(self.audios_missing)(FilterMediaScope::Season(&season), missing_audios)? - { - return Ok(vec![]); - } - seasons.push(season) - } - - let duplicated_seasons = get_duplicated_seasons(&seasons); - if !duplicated_seasons.is_empty() { - if self.interactive_input { - check_for_duplicated_seasons(&mut seasons) - } else { - info!( - "Found duplicated seasons: {}", - duplicated_seasons - .iter() - .map(|d| d.to_string()) - .collect::>() - .join(", ") - ) - } - } - - self.series_visited = true; - - Ok(seasons) - } - - async fn visit_season(&mut self, season: Season) -> Result> { - if !self.url_filter.is_season_valid(season.season_number) { - return Ok(vec![]); - } - - let mut seasons = vec![]; - if self - .audio_locales - .iter() - .any(|l| season.audio_locales.contains(l)) - { - seasons.push(season.clone()) - } - for version in season.versions { - if season.id == version.id { - continue; - } - if self.audio_locales.contains(&version.audio_locale) { - seasons.push(version.season().await?) - } - } - - let mut episodes = vec![]; - for season in seasons { - self.season_sorting.push(season.id.clone()); - let mut eps = season.episodes().await?; - - // removes any episode that does not have the audio locale of the season. yes, this is - // the case sometimes - if season.audio_locales.len() < 2 { - let season_locale = season - .audio_locales - .first() - .cloned() - .unwrap_or(Locale::ja_JP); - eps.retain(|e| e.audio_locale == season_locale) - } - - #[allow(clippy::if_same_then_else)] - if eps.len() < season.number_of_episodes as usize { - if eps.is_empty() - && !(self.audios_missing)( - FilterMediaScope::Season(&season), - season.audio_locales.iter().collect(), - )? - { - return Ok(vec![]); - } else if !eps.is_empty() - && !(self.audios_missing)( - FilterMediaScope::Episode(vec![eps.first().unwrap(), eps.last().unwrap()]), - vec![&eps.first().unwrap().audio_locale], - )? - { - return Ok(vec![]); - } - } - - episodes.extend(eps) - } - - if self.relative_episode_number { - for episode in &episodes { - self.season_episodes - .entry(episode.season_id.clone()) - .or_default() - .push(episode.clone()) - } - } - - Ok(episodes) - } - - async fn visit_episode(&mut self, episode: Episode) -> Result> { - if !self - .url_filter - .is_episode_valid(episode.sequence_number, episode.season_number) - { - return Ok(vec![]); - } - - // skip the episode if it's a special - if self.skip_specials - && (episode.sequence_number == 0.0 || episode.sequence_number.fract() != 0.0) - { - return Ok(vec![]); - } - - let mut episodes = vec![]; - if !self.series_visited { - if self.audio_locales.contains(&episode.audio_locale) { - episodes.push(episode.clone()) - } - for version in &episode.versions { - // `episode` is also a version of itself. the if block above already adds the - // episode if it matches the requested audio, so it doesn't need to be requested - // here again - if version.id == episode.id { - continue; - } - if self.audio_locales.contains(&version.audio_locale) { - episodes.push(version.episode().await?) - } - } - - let audio_locales: Vec = - episodes.iter().map(|e| e.audio_locale.clone()).collect(); - let missing_audios = missing_locales(&audio_locales, &self.audio_locales); - if !missing_audios.is_empty() - && !(self.audios_missing)( - FilterMediaScope::Episode(vec![&episode]), - missing_audios, - )? - { - return Ok(vec![]); - } - - let mut subtitle_locales: Vec = episodes - .iter() - .flat_map(|e| e.subtitle_locales.clone()) - .collect(); - subtitle_locales.sort(); - subtitle_locales.dedup(); - let missing_subtitles = missing_locales(&subtitle_locales, &self.subtitle_locales); - if !missing_subtitles.is_empty() - && !(self.subtitles_missing)( - FilterMediaScope::Episode(vec![&episode]), - missing_subtitles, - )? - { - return Ok(vec![]); - } - } else { - episodes.push(episode.clone()) - } - - if let Some(seasons_with_premium) = &mut self.season_with_premium { - let episodes_len_before = episodes.len(); - episodes.retain(|e| !e.is_premium_only && !self.is_premium); - if episodes_len_before < episodes.len() - && !seasons_with_premium.contains(&episode.season_number) - { - (self.no_premium)(episode.season_number)?; - seasons_with_premium.push(episode.season_number) - } - - if episodes.is_empty() { - return Ok(vec![]); - } - } - - let mut relative_episode_number = None; - let mut relative_sequence_number = None; - if self.relative_episode_number { - let season_eps = match self.season_episodes.get(&episode.season_id) { - Some(eps) => eps, - None => { - self.season_episodes.insert( - episode.season_id.clone(), - episode.season().await?.episodes().await?, - ); - self.season_episodes.get(&episode.season_id).unwrap() - } - }; - let mut non_integer_sequence_number_count = 0; - for (i, ep) in season_eps.iter().enumerate() { - if ep.sequence_number != 0.0 || ep.sequence_number.fract() == 0.0 { - non_integer_sequence_number_count += 1 - } - if ep.id == episode.id { - relative_episode_number = Some(i + 1); - relative_sequence_number = Some( - (i + 1 - non_integer_sequence_number_count) as f32 - + fract(ep.sequence_number), - ); - break; - } - } - if relative_episode_number.is_none() || relative_sequence_number.is_none() { - warn!( - "Failed to get relative episode number for episode {} ({}) of {} season {}", - episode.sequence_number, - episode.title, - episode.series_title, - episode.season_number, - ) - } - } - - Ok(episodes - .into_iter() - .map(|e| { - SingleFormat::new_from_episode( - e.clone(), - e.subtitle_locales, - relative_episode_number.map(|n| n as u32), - relative_sequence_number, - ) - }) - .collect()) - } - - async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result> { - Ok(movie_listing.movies().await?) - } - - async fn visit_movie(&mut self, movie: Movie) -> Result> { - Ok(vec![SingleFormat::new_from_movie(movie, vec![])]) - } - - async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result> { - Ok(vec![SingleFormat::new_from_music_video(music_video)]) - } - - async fn visit_concert(&mut self, concert: Concert) -> Result> { - Ok(vec![SingleFormat::new_from_concert(concert)]) - } - - async fn finish(self, input: Vec>) -> Result { - let flatten_input: Vec = input.into_iter().flatten().collect(); - - let mut single_format_collection = SingleFormatCollection::new(); - - let mut pre_sorted: BTreeMap> = BTreeMap::new(); - for data in flatten_input { - pre_sorted - .entry(data.identifier.clone()) - .or_default() - .push(data) - } - - let mut sorted: Vec<(String, Vec)> = pre_sorted.into_iter().collect(); - sorted.sort_by(|(_, a), (_, b)| { - self.season_sorting - .iter() - .position(|p| p == &a.first().unwrap().season_id) - .unwrap() - .cmp( - &self - .season_sorting - .iter() - .position(|p| p == &b.first().unwrap().season_id) - .unwrap(), - ) - }); - - for (_, mut data) in sorted { - data.sort_by(|a, b| { - self.audio_locales - .iter() - .position(|p| p == &a.audio) - .unwrap_or(usize::MAX) - .cmp( - &self - .audio_locales - .iter() - .position(|p| p == &b.audio) - .unwrap_or(usize::MAX), - ) - }); - single_format_collection.add_single_formats(data) - } - - Ok(single_format_collection) - } - - pub(crate) async fn visit( - mut self, - media_collection: MediaCollection, - ) -> Result { + async fn visit(mut self, media_collection: MediaCollection) -> Result + where + Self: Send + Sized, + { let mut items = vec![media_collection]; let mut result = vec![]; @@ -425,7 +42,9 @@ impl Filter { .collect::>(), ), MediaCollection::Episode(episode) => { - result.push(self.visit_episode(episode).await?) + if let Some(t) = self.visit_episode(episode).await? { + result.push(t) + } } MediaCollection::MovieListing(movie_listing) => new_items.extend( self.visit_movie_listing(movie_listing) @@ -434,12 +53,20 @@ impl Filter { .map(|m| m.into()) .collect::>(), ), - MediaCollection::Movie(movie) => result.push(self.visit_movie(movie).await?), + MediaCollection::Movie(movie) => { + if let Some(t) = self.visit_movie(movie).await? { + result.push(t) + } + } MediaCollection::MusicVideo(music_video) => { - result.push(self.visit_music_video(music_video).await?) + if let Some(t) = self.visit_music_video(music_video).await? { + result.push(t) + } } MediaCollection::Concert(concert) => { - result.push(self.visit_concert(concert).await?) + if let Some(t) = self.visit_concert(concert).await? { + result.push(t) + } } } } @@ -449,10 +76,8 @@ impl Filter { self.finish(result).await } -} -fn missing_locales<'a>(available: &[Locale], searched: &'a [Locale]) -> Vec<&'a Locale> { - searched.iter().filter(|p| !available.contains(p)).collect() + async fn finish(self, input: Vec) -> Result; } /// Remove all duplicates from a [`Vec`]. diff --git a/crunchy-cli-core/src/utils/format.rs b/crunchy-cli-core/src/utils/format.rs index 33ce261..0a71838 100644 --- a/crunchy-cli-core/src/utils/format.rs +++ b/crunchy-cli-core/src/utils/format.rs @@ -4,7 +4,7 @@ use crate::utils::log::tab_info; use crate::utils::os::{is_special_file, sanitize}; use anyhow::{bail, Result}; use chrono::{Datelike, Duration}; -use crunchyroll_rs::media::{SkipEvents, Stream, StreamData, Subtitle}; +use crunchyroll_rs::media::{Resolution, SkipEvents, Stream, StreamData, Subtitle}; use crunchyroll_rs::{Concert, Episode, Locale, MediaCollection, Movie, MusicVideo}; use log::{debug, info}; use std::cmp::Ordering; @@ -12,7 +12,6 @@ use std::collections::BTreeMap; use std::env; use std::path::{Path, PathBuf}; -#[allow(dead_code)] #[derive(Clone)] pub struct SingleFormat { pub identifier: String, @@ -168,19 +167,18 @@ impl SingleFormat { pub async fn stream(&self) -> Result { let stream = match &self.source { - MediaCollection::Episode(e) => e.stream_maybe_without_drm().await, - MediaCollection::Movie(m) => m.stream_maybe_without_drm().await, - MediaCollection::MusicVideo(mv) => mv.stream_maybe_without_drm().await, - MediaCollection::Concert(c) => c.stream_maybe_without_drm().await, + MediaCollection::Episode(e) => e.stream_maybe_without_drm().await?, + MediaCollection::Movie(m) => m.stream_maybe_without_drm().await?, + MediaCollection::MusicVideo(mv) => mv.stream_maybe_without_drm().await?, + MediaCollection::Concert(c) => c.stream_maybe_without_drm().await?, _ => unreachable!(), }; - if let Err(crunchyroll_rs::error::Error::Request { message, .. }) = &stream { - if message.starts_with("TOO_MANY_ACTIVE_STREAMS") { - bail!("Too many active/parallel streams. Please close at least one stream you're watching and try again") - } - }; - Ok(stream?) + if stream.session.uses_stream_limits { + bail!("Found a stream which probably uses DRM. DRM downloads aren't supported") + } + + Ok(stream) } pub async fn skip_events(&self) -> Result> { @@ -348,7 +346,6 @@ impl Iterator for SingleFormatCollectionIterator { } } -#[allow(dead_code)] #[derive(Clone)] pub struct Format { pub title: String, @@ -356,6 +353,8 @@ pub struct Format { pub locales: Vec<(Locale, Vec)>, + // deprecated + pub resolution: Resolution, pub width: u64, pub height: u64, pub fps: f64, @@ -401,6 +400,7 @@ impl Format { title: first_format.title, description: first_format.description, locales, + resolution: first_stream.resolution().unwrap(), width: first_stream.resolution().unwrap().width, height: first_stream.resolution().unwrap().height, fps: first_stream.fps().unwrap(), @@ -448,11 +448,11 @@ impl Format { ) .replace( "{width}", - &sanitize(self.width.to_string(), true, universal), + &sanitize(self.resolution.width.to_string(), true, universal), ) .replace( "{height}", - &sanitize(self.height.to_string(), true, universal), + &sanitize(self.resolution.height.to_string(), true, universal), ) .replace("{series_id}", &sanitize(&self.series_id, true, universal)) .replace( @@ -588,7 +588,7 @@ impl Format { .collect::>() .join(", ") ); - tab_info!("Resolution: {}x{}", self.height, self.width); + tab_info!("Resolution: {}", self.resolution); tab_info!("FPS: {:.2}", self.fps) } diff --git a/crunchy-cli-core/src/utils/video.rs b/crunchy-cli-core/src/utils/video.rs index a15296c..07f6e76 100644 --- a/crunchy-cli-core/src/utils/video.rs +++ b/crunchy-cli-core/src/utils/video.rs @@ -5,12 +5,28 @@ use crunchyroll_rs::Locale; pub async fn stream_data_from_stream( stream: &Stream, resolution: &Resolution, - hardsub_subtitle: Option, + subtitle: Option, ) -> Result> { - let (hardsub_locale, mut contains_hardsub) = if hardsub_subtitle.is_some() { - (hardsub_subtitle, true) + // sometimes Crunchyroll marks episodes without real subtitles that they have subtitles and + // reports that only hardsub episode are existing. the following lines are trying to prevent + // potential errors which might get caused by this incorrect reporting + // (https://github.com/crunchy-labs/crunchy-cli/issues/231) + let mut hardsub_locales: Vec = stream.hard_subs.keys().cloned().collect(); + let (hardsub_locale, mut contains_hardsub) = if !hardsub_locales + .contains(&Locale::Custom("".to_string())) + && !hardsub_locales.contains(&Locale::Custom(":".to_string())) + { + // if only one hardsub locale exists, assume that this stream doesn't really contains hardsubs + if hardsub_locales.len() == 1 { + (Some(hardsub_locales.remove(0)), false) + } else { + // fallback to `None`. this should trigger an error message in `stream.dash_streaming_data` + // that the requested stream is not available + (None, false) + } } else { - (None, false) + let hardsubs_requested = subtitle.is_some(); + (subtitle, hardsubs_requested) }; let (mut videos, mut audios) = match stream.stream_data(hardsub_locale).await { @@ -27,11 +43,6 @@ pub async fn stream_data_from_stream( } } .unwrap(); - - if videos.iter().any(|v| v.drm.is_some()) || audios.iter().any(|v| v.drm.is_some()) { - bail!("Stream is DRM protected") - } - videos.sort_by(|a, b| a.bandwidth.cmp(&b.bandwidth).reverse()); audios.sort_by(|a, b| a.bandwidth.cmp(&b.bandwidth).reverse());