Compare commits

..

24 commits

Author SHA1 Message Date
Simon
4332b1beef
not add start time when syncing (#442)
* not add start time when syncing

* use itsoffset for all syncing related time shifts
2024-07-01 18:43:16 +02:00
bytedream
2cf9125de3 Update README.md 2024-07-01 16:38:29 +02:00
bytedream
756022b955 Fix panic when in anonymously 2024-06-20 00:12:33 +02:00
bytedream
509683d23a Update dependencies and version 2024-06-19 23:38:57 +02:00
bytedream
8047680799 Add drm check 2024-06-19 23:18:35 +02:00
bytedream
287df84382 Rework episode filtering 2024-06-14 00:21:07 +02:00
bytedream
e7ac6d8874 Deprecate search stream.is_drm option 2024-05-24 22:17:25 +02:00
bytedream
fb8e535644 Fix subtitle title not being human-readable 2024-05-24 22:09:23 +02:00
bytedream
67c267be20 Remove unused variable 2024-05-24 22:05:04 +02:00
bytedream
a1c7b2069d Update dependencies and version 2024-05-23 00:01:42 +02:00
bytedream
74e5e05b0f Invalidate stream when using search command (#428) 2024-05-22 23:59:12 +02:00
bytedream
7d2ae719c8 Remove internal jwt error retry 2024-05-22 16:54:58 +02:00
bytedream
5593046aae Update dependencies and version 2024-05-22 16:52:43 +02:00
bytedream
f8bd092987 Add custom error message if too many streams are active 2024-05-21 21:51:18 +02:00
bytedream
cbe57e2b6e Update dependencies and version 2024-05-21 21:34:05 +02:00
bytedream
f7ce888329 Bypass stream limits 2024-05-21 21:33:08 +02:00
bytedream
301dac478f Update dependencies and version 2024-05-20 15:57:28 +02:00
bytedream
9819b62259 Fix typo in additional subtitle field (#421) 2024-05-17 23:45:41 +02:00
bytedream
5279a9b759 Update dependencies and version 2024-05-14 23:59:01 +02:00
bytedream
a98e31f959 Only include one CC subtitle 2024-05-14 22:36:59 +02:00
bytedream
590242712b Add warning message the --skip-existing-method has no effect without --skip-existing (#418) 2024-05-14 21:36:12 +02:00
bytedream
817963af4f Fix video containing hardsub if not requested (#415) 2024-05-14 21:22:23 +02:00
bytedream
48bb7a5ef6 Fix crashes when converting subtitles (#408) 2024-05-14 16:11:55 +02:00
Simon
53a710a373
Fix audio syncing using wrong internal index (#407) 2024-05-07 16:13:10 +02:00
16 changed files with 707 additions and 1008 deletions

153
Cargo.lock generated
View file

@ -92,9 +92,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.83"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "async-speed-limit"
@ -119,6 +119,12 @@ 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"
@ -188,9 +194,9 @@ checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cc"
version = "1.0.97"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
[[package]]
name = "cfg-if"
@ -238,7 +244,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim 0.11.1",
"strsim",
]
[[package]]
@ -343,7 +349,7 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "crunchy-cli"
version = "3.6.1"
version = "3.6.7"
dependencies = [
"chrono",
"clap",
@ -356,7 +362,7 @@ dependencies = [
[[package]]
name = "crunchy-cli-core"
version = "3.6.1"
version = "3.6.7"
dependencies = [
"anyhow",
"async-speed-limit",
@ -386,6 +392,7 @@ dependencies = [
"shlex",
"sys-locale",
"tempfile",
"time",
"tokio",
"tokio-util",
"tower-service",
@ -393,9 +400,9 @@ dependencies = [
[[package]]
name = "crunchyroll-rs"
version = "0.11.1"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58580acc9c0abf96a231ec8b1a4597ea55d9426ea17f684ce3582e2b26437bbb"
checksum = "d6e38c223aecf65c9c9bec50764beea5dc70b6c97cd7f767bf6860f2fc8e0a07"
dependencies = [
"async-trait",
"chrono",
@ -419,9 +426,9 @@ dependencies = [
[[package]]
name = "crunchyroll-rs-internal"
version = "0.11.1"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce3c844dec8a3390f8c9853b5cf1d65c3d38fd0657b8b5d0e008db8945dea326"
checksum = "144a38040a21aaa456741a9f6749354527bb68ad3bb14210e0bbc40fbd95186c"
dependencies = [
"darling",
"quote",
@ -440,9 +447,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.20.8"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1"
dependencies = [
"darling_core",
"darling_macro",
@ -450,23 +457,23 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.8"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.8"
version = "0.20.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178"
dependencies = [
"darling_core",
"quote",
@ -475,12 +482,13 @@ dependencies = [
[[package]]
name = "dash-mpd"
version = "0.16.1"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79b4bdd5f1c0c7493d780c645f0bff5b9361e6408210fa88910adb181efca64c"
checksum = "4618a5e165bf47b084963611bcf1d568c681f52d8a237e8862a0cd8c546ba255"
dependencies = [
"base64 0.22.1",
"base64-serde",
"bytes",
"chrono",
"fs-err",
"iso8601",
@ -553,9 +561,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
[[package]]
name = "encode_unicode"
@ -580,9 +588,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
dependencies = [
"libc",
"windows-sys 0.52.0",
@ -731,15 +739,15 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "h2"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069"
checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
dependencies = [
"atomic-waker",
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap 2.2.6",
"slab",
@ -977,9 +985,9 @@ dependencies = [
[[package]]
name = "instant"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
@ -1041,9 +1049,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.154"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libredox"
@ -1057,9 +1065,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "log"
@ -1097,9 +1105,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
dependencies = [
"adler",
]
@ -1117,8 +1125,8 @@ dependencies = [
[[package]]
name = "native-tls"
version = "0.2.11"
source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=fdba246#fdba246a79986607cbdf573733445498bb6da2a9"
version = "0.2.12"
source = "git+https://github.com/crunchy-labs/rust-not-so-native-tls.git?rev=c7ac566#c7ac566559d441bbc3e5e5bd04fb7162c38d88b0"
dependencies = [
"libc",
"log",
@ -1155,9 +1163,9 @@ dependencies = [
[[package]]
name = "num-complex"
version = "0.4.5"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
@ -1251,9 +1259,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.2.3+3.2.1"
version = "300.3.0+3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843"
checksum = "eba8804a1c5765b18c4b3f907e6897ebabeedebc9830e1a0046c4a4cf44663e1"
dependencies = [
"cc",
]
@ -1344,9 +1352,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.81"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
dependencies = [
"unicode-ident",
]
@ -1511,12 +1519,13 @@ checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
[[package]]
name = "rsubs-lib"
version = "0.2.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dcca2a9560fca05de8f95bc3767e46673d4b4c1f2c7a11092e10efd95bbdf62"
checksum = "8c9f50e3fbcbf1f0bd109954e2dd813d1715c7b4a92a7bf159a85dea49e9d863"
dependencies = [
"regex",
"serde",
"time",
]
[[package]]
@ -1533,9 +1542,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.23"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustfft"
@ -1604,15 +1613,15 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.5.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
[[package]]
name = "rustls-webpki"
version = "0.102.3"
version = "0.102.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf"
checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e"
dependencies = [
"ring",
"rustls-pki-types",
@ -1631,9 +1640,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "schannel"
@ -1669,18 +1678,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.200"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.200"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
@ -1689,9 +1698,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.116"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
@ -1819,12 +1828,6 @@ 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"
@ -1839,9 +1842,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "2.0.60"
version = "2.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106"
dependencies = [
"proc-macro2",
"quote",
@ -1898,18 +1901,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.59"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.59"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@ -1964,9 +1967,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
@ -1981,9 +1984,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",

View file

@ -1,7 +1,7 @@
[package]
name = "crunchy-cli"
authors = ["Crunchy Labs Maintainers"]
version = "3.6.1"
version = "3.6.7"
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.37", features = ["macros", "rt-multi-thread", "time"], default-features = false }
tokio = { version = "1.38", features = ["macros", "rt-multi-thread", "time"], default-features = false }
native-tls-crate = { package = "native-tls", version = "0.2.11", optional = true }
native-tls-crate = { package = "native-tls", version = "0.2.12", 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 = "fdba246" }
native-tls = { git = "https://github.com/crunchy-labs/rust-not-so-native-tls.git", rev = "c7ac566" }
[profile.release]
strip = true

View file

@ -1,6 +1,4 @@
> ~~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.
# This project has been sunset as Crunchyroll moved to a DRM-only system. See [#362](https://github.com/crunchy-labs/crunchy-cli/issues/362).
# crunchy-cli

View file

@ -1,7 +1,7 @@
[package]
name = "crunchy-cli-core"
authors = ["Crunchy Labs Maintainers"]
version = "3.6.1"
version = "3.6.7"
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.1", features = ["experimental-stabilizations", "tower"] }
crunchyroll-rs = { version = "0.11.4", 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.2.1"
rsubs-lib = "~0.3.2"
rusty-chromaprint = "0.2"
serde = "1.0"
serde_json = "1.0"
@ -38,7 +38,8 @@ serde_plain = "1.0"
shlex = "1.3"
sys-locale = "0.3"
tempfile = "3.10"
tokio = { version = "1.37", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] }
time = "0.3"
tokio = { version = "1.38", 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 }

View file

@ -1,10 +1,9 @@
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;
use crate::utils::filter::{Filter, FilterMediaScope};
use crate::utils::format::{Format, SingleFormat};
use crate::utils::locale::{all_locale_in_locales, resolve_locales, LanguageTagging};
use crate::utils::log::progress;
@ -234,6 +233,10 @@ 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());
@ -280,9 +283,49 @@ 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 = ArchiveFilter::new(
let single_format_collection = Filter::new(
url_filter,
self.clone(),
self.audio.clone(),
self.subtitle.clone(),
|scope, locales| {
let audios = locales.into_iter().map(|l| l.to_string()).collect::<Vec<String>>().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::<Vec<String>>().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.yes,
self.skip_specials,
ctx.crunchy.premium().await,
@ -497,29 +540,28 @@ async fn get_format(
.subtitle
.iter()
.flat_map(|s| {
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));
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))
}
}
subtitles
.into_iter()
.chain(cc.into_iter())
.collect::<Vec<(Subtitle, bool)>>()
})
.collect();
format_pairs.push((single_format, video.clone(), audio, subtitles.clone()));
single_format_to_format_pairs.push((single_format.clone(), video, subtitles))
single_format_to_format_pairs.push((single_format.clone(), video, subtitles));
stream.invalidate().await?
}
let mut download_formats = vec![];

View file

@ -1,466 +0,0 @@
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<String, Vec<Episode>>,
season_subtitles_missing: Vec<u32>,
seasons_with_premium: Option<Vec<u32>>,
season_sorting: Vec<String>,
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<SingleFormat>;
type Output = SingleFormatCollection;
async fn visit_series(&mut self, series: Series) -> Result<Vec<Season>> {
// `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::<Vec<String>>()
.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::<Vec<String>>()
.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::<Vec<String>>()
.join(", ")
)
}
}
Ok(seasons)
}
async fn visit_season(&mut self, mut season: Season) -> Result<Vec<Episode>> {
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<Locale> = 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::<Vec<String>>()
.join(", ")
)
}
let subtitle_locales: Vec<Locale> = 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::<Vec<String>>()
.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<Option<Self::T>> {
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<Locale> = 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::<Vec<String>>()
.join(", ")
)
}
let mut subtitle_locales: Vec<Locale> =
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::<Vec<String>>()
.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<Vec<Movie>> {
Ok(movie_listing.movies().await?)
}
async fn visit_movie(&mut self, movie: Movie) -> Result<Option<Self::T>> {
Ok(Some(vec![SingleFormat::new_from_movie(movie, vec![])]))
}
async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result<Option<Self::T>> {
Ok(Some(vec![SingleFormat::new_from_music_video(music_video)]))
}
async fn visit_concert(&mut self, concert: Concert) -> Result<Option<Self::T>> {
Ok(Some(vec![SingleFormat::new_from_concert(concert)]))
}
async fn finish(self, input: Vec<Self::T>) -> Result<Self::Output> {
let flatten_input: Self::T = input.into_iter().flatten().collect();
let mut single_format_collection = SingleFormatCollection::new();
let mut pre_sorted: BTreeMap<String, Self::T> = 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()
}

View file

@ -1,4 +1,3 @@
mod command;
mod filter;
pub use command::Archive;

View file

@ -1,8 +1,7 @@
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;
use crate::utils::filter::{Filter, FilterMediaScope};
use crate::utils::format::{Format, SingleFormat};
use crate::utils::locale::{resolve_locales, LanguageTagging};
use crate::utils::log::progress;
@ -14,7 +13,7 @@ use anyhow::bail;
use anyhow::Result;
use crunchyroll_rs::media::Resolution;
use crunchyroll_rs::Locale;
use log::{debug, warn};
use log::{debug, error, warn};
use std::collections::HashMap;
use std::path::Path;
@ -250,9 +249,53 @@ 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 = DownloadFilter::new(
let single_format_collection = Filter::new(
url_filter,
self.clone(),
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.yes,
self.skip_specials,
ctx.crunchy.premium().await,
@ -384,12 +427,20 @@ 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)
.cloned()
// use closed captions as fallback if no actual subtitles are found
.or_else(|| stream.captions.get(subtitle_locale).cloned())
.or_else(|| stream.captions.get(subtitle_locale))
.cloned()
} else {
stream
.captions
.get(subtitle_locale)
.or_else(|| stream.subtitles.get(subtitle_locale))
.cloned()
}
} else {
None
};
@ -426,5 +477,7 @@ async fn get_format(
subs.push(download.subtitle.clone().unwrap())
}
stream.invalidate().await?;
Ok((download_format, format))
}

View file

@ -1,307 +0,0 @@
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<u32, Vec<Episode>>,
season_subtitles_missing: Vec<u32>,
seasons_with_premium: Option<Vec<u32>>,
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<Vec<Season>> {
// `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::<Vec<String>>()
.join(", ")
)
}
}
Ok(seasons)
}
async fn visit_season(&mut self, season: Season) -> Result<Vec<Episode>> {
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<Option<Self::T>> {
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<Vec<Movie>> {
Ok(movie_listing.movies().await?)
}
async fn visit_movie(&mut self, movie: Movie) -> Result<Option<Self::T>> {
Ok(Some(SingleFormat::new_from_movie(movie, vec![])))
}
async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result<Option<Self::T>> {
Ok(Some(SingleFormat::new_from_music_video(music_video)))
}
async fn visit_concert(&mut self, concert: Concert) -> Result<Option<Self::T>> {
Ok(Some(SingleFormat::new_from_concert(concert)))
}
async fn finish(self, input: Vec<Self::T>) -> Result<Self::Output> {
let mut single_format_collection = SingleFormatCollection::new();
for data in input {
single_format_collection.add_single_formats(vec![data])
}
Ok(single_format_collection)
}
}

View file

@ -1,4 +1,3 @@
mod command;
mod filter;
pub use command::Download;

View file

@ -111,6 +111,10 @@ 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],

View file

@ -173,7 +173,7 @@ impl From<&Stream> for FormatStream {
Self {
locale: value.audio_locale.clone(),
dash_url: value.url.clone(),
is_drm: value.session.uses_stream_limits,
is_drm: false,
}
}
}
@ -241,14 +241,6 @@ 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<usize>, Scope, String)>,
pattern_count: HashMap<Scope, u32>,
@ -421,7 +413,15 @@ impl Format {
};
let mut seasons = vec![];
for season in tmp_seasons {
seasons.extend(self_and_versions!(season => self.filter_options.audio.clone()))
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?)
}
}
}
tree.extend(
self.filter_options
@ -435,7 +435,15 @@ impl Format {
if !episode_empty || !stream_empty {
match &media_collection {
MediaCollection::Episode(episode) => {
let episodes = self_and_versions!(episode => self.filter_options.audio.clone());
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?)
}
}
tree.push((
Season::default(),
episodes
@ -464,7 +472,9 @@ impl Format {
if !stream_empty {
for (_, episodes) in tree.iter_mut() {
for (episode, streams) in episodes {
streams.push(episode.stream_maybe_without_drm().await?)
let stream = episode.stream_maybe_without_drm().await?;
stream.clone().invalidate().await?;
streams.push(stream)
}
}
} else {

View file

@ -13,17 +13,19 @@ 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;
@ -320,35 +322,25 @@ impl Downloader {
if let Some(offsets) = offsets {
let mut root_format_idx = 0;
let mut root_format_length = 0;
let mut audio_count: usize = 0;
let mut subtitle_count: usize = 0;
let mut root_format_offset = u64::MAX;
for (i, format) in self.formats.iter().enumerate() {
let offset = offsets.get(&i).copied().unwrap_or_default();
let format_len = format
.video
.0
.segments()
.iter()
.map(|s| s.length.as_millis())
.sum::<u128>() as u64
- offset.num_milliseconds() as u64;
if format_len > root_format_length {
let format_offset = offset.num_milliseconds() as u64;
if format_offset < root_format_offset {
root_format_idx = i;
root_format_length = format_len;
root_format_offset = format_offset;
}
for _ in &format.audios {
if let Some(offset) = &offsets.get(&i) {
audio_offsets.insert(audio_count, **offset);
audio_offsets.insert(i, **offset);
}
audio_count += 1
}
for _ in &format.subtitles {
if let Some(offset) = &offsets.get(&i) {
subtitle_offsets.insert(subtitle_count, **offset);
subtitle_offsets.insert(i, **offset);
}
subtitle_count += 1
}
}
@ -569,7 +561,7 @@ impl Downloader {
for (i, meta) in videos.iter().enumerate() {
if let Some(start_time) = meta.start_time {
input.extend(["-ss".to_string(), format_time_delta(&start_time)])
input.extend(["-itsoffset".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()]);
@ -590,7 +582,7 @@ impl Downloader {
}
for (i, meta) in audios.iter().enumerate() {
if let Some(start_time) = meta.start_time {
input.extend(["-ss".to_string(), format_time_delta(&start_time)])
input.extend(["-itsoffset".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()]);
@ -637,7 +629,7 @@ impl Downloader {
if container_supports_softsubs {
for (i, meta) in subtitles.iter().enumerate() {
if let Some(start_time) = meta.start_time {
input.extend(["-ss".to_string(), format_time_delta(&start_time)])
input.extend(["-itsoffset".to_string(), format_time_delta(&start_time)])
}
input.extend(["-i".to_string(), meta.path.to_string_lossy().to_string()]);
maps.extend([
@ -656,7 +648,7 @@ impl Downloader {
metadata.extend([
format!("-metadata:s:s:{}", i),
format!("title={}", {
let mut title = meta.locale.to_string();
let mut title = meta.locale.to_human_readable();
if meta.cc {
title += " (CC)"
}
@ -933,36 +925,43 @@ impl Downloader {
) -> Result<TempPath> {
let buf = subtitle.data().await?;
let mut ass = match subtitle.format.as_str() {
"ass" => ssa::parse(String::from_utf8_lossy(&buf).to_string()),
"vtt" => vtt::parse(String::from_utf8_lossy(&buf).to_string()).to_ass(),
"ass" => SSA::parse(String::from_utf8_lossy(&buf))?,
"vtt" => VTT::parse(String::from_utf8_lossy(&buf))?.to_ssa(),
_ => 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
ass.events
.sort_by(|a, b| a.line_start.total_ms().cmp(&b.line_start.total_ms()));
// (https://github.com/crunchy-labs/crunchy-cli/issues/208)
ass.events.sort_by(|a, b| a.start.cmp(&b.start));
// 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 maxim
// to be at most as long as `max_length`
// (https://github.com/crunchy-labs/crunchy-cli/issues/32)
for i in (0..ass.events.len()).rev() {
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);
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
}
ass.events[i]
.line_end
.set_ms(max_length.num_milliseconds() as u32);
ass.events[i].end = max_len
} 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();
ass.to_file(path.to_string_lossy().to_string().as_str())?;
fs::write(&path, ass.to_string())?;
Ok(path)
}

View file

@ -1,24 +1,407 @@
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, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series,
Concert, Episode, Locale, MediaCollection, Movie, MovieListing, MusicVideo, Season, Series,
};
use log::{info, warn};
use std::collections::{BTreeMap, HashMap};
use std::ops::Not;
pub trait Filter {
type T: Send + Sized;
type Output: Send + Sized;
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>),
}
async fn visit_series(&mut self, series: Series) -> Result<Vec<Season>>;
async fn visit_season(&mut self, season: Season) -> Result<Vec<Episode>>;
async fn visit_episode(&mut self, episode: Episode) -> Result<Option<Self::T>>;
async fn visit_movie_listing(&mut self, movie_listing: MovieListing) -> Result<Vec<Movie>>;
async fn visit_movie(&mut self, movie: Movie) -> Result<Option<Self::T>>;
async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result<Option<Self::T>>;
async fn visit_concert(&mut self, concert: Concert) -> Result<Option<Self::T>>;
pub(crate) struct Filter {
url_filter: UrlFilter,
async fn visit(mut self, media_collection: MediaCollection) -> Result<Self::Output>
where
Self: Send + Sized,
skip_specials: bool,
interactive_input: bool,
relative_episode_number: bool,
audio_locales: Vec<Locale>,
subtitle_locales: Vec<Locale>,
audios_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result<bool>,
subtitles_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result<bool>,
no_premium: fn(u32) -> Result<()>,
is_premium: bool,
series_visited: bool,
season_episodes: HashMap<String, Vec<Episode>>,
season_with_premium: Option<Vec<u32>>,
season_sorting: Vec<String>,
}
impl Filter {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
url_filter: UrlFilter,
audio_locales: Vec<Locale>,
subtitle_locales: Vec<Locale>,
audios_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result<bool>,
subtitles_missing: fn(FilterMediaScope, Vec<&Locale>) -> Result<bool>,
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<Vec<Season>> {
// 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::<Vec<Locale>>(),
&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::<Vec<String>>()
.join(", ")
)
}
}
self.series_visited = true;
Ok(seasons)
}
async fn visit_season(&mut self, season: Season) -> Result<Vec<Episode>> {
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<Vec<SingleFormat>> {
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<Locale> =
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<Locale> = 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<Vec<Movie>> {
Ok(movie_listing.movies().await?)
}
async fn visit_movie(&mut self, movie: Movie) -> Result<Vec<SingleFormat>> {
Ok(vec![SingleFormat::new_from_movie(movie, vec![])])
}
async fn visit_music_video(&mut self, music_video: MusicVideo) -> Result<Vec<SingleFormat>> {
Ok(vec![SingleFormat::new_from_music_video(music_video)])
}
async fn visit_concert(&mut self, concert: Concert) -> Result<Vec<SingleFormat>> {
Ok(vec![SingleFormat::new_from_concert(concert)])
}
async fn finish(self, input: Vec<Vec<SingleFormat>>) -> Result<SingleFormatCollection> {
let flatten_input: Vec<SingleFormat> = input.into_iter().flatten().collect();
let mut single_format_collection = SingleFormatCollection::new();
let mut pre_sorted: BTreeMap<String, Vec<SingleFormat>> = BTreeMap::new();
for data in flatten_input {
pre_sorted
.entry(data.identifier.clone())
.or_default()
.push(data)
}
let mut sorted: Vec<(String, Vec<SingleFormat>)> = 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<SingleFormatCollection> {
let mut items = vec![media_collection];
let mut result = vec![];
@ -42,9 +425,7 @@ pub trait Filter {
.collect::<Vec<MediaCollection>>(),
),
MediaCollection::Episode(episode) => {
if let Some(t) = self.visit_episode(episode).await? {
result.push(t)
}
result.push(self.visit_episode(episode).await?)
}
MediaCollection::MovieListing(movie_listing) => new_items.extend(
self.visit_movie_listing(movie_listing)
@ -53,20 +434,12 @@ pub trait Filter {
.map(|m| m.into())
.collect::<Vec<MediaCollection>>(),
),
MediaCollection::Movie(movie) => {
if let Some(t) = self.visit_movie(movie).await? {
result.push(t)
}
}
MediaCollection::Movie(movie) => result.push(self.visit_movie(movie).await?),
MediaCollection::MusicVideo(music_video) => {
if let Some(t) = self.visit_music_video(music_video).await? {
result.push(t)
}
result.push(self.visit_music_video(music_video).await?)
}
MediaCollection::Concert(concert) => {
if let Some(t) = self.visit_concert(concert).await? {
result.push(t)
}
result.push(self.visit_concert(concert).await?)
}
}
}
@ -76,8 +449,10 @@ pub trait Filter {
self.finish(result).await
}
}
async fn finish(self, input: Vec<Self::T>) -> Result<Self::Output>;
fn missing_locales<'a>(available: &[Locale], searched: &'a [Locale]) -> Vec<&'a Locale> {
searched.iter().filter(|p| !available.contains(p)).collect()
}
/// Remove all duplicates from a [`Vec`].

View file

@ -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::{Resolution, SkipEvents, Stream, StreamData, Subtitle};
use crunchyroll_rs::media::{SkipEvents, Stream, StreamData, Subtitle};
use crunchyroll_rs::{Concert, Episode, Locale, MediaCollection, Movie, MusicVideo};
use log::{debug, info};
use std::cmp::Ordering;
@ -12,6 +12,7 @@ use std::collections::BTreeMap;
use std::env;
use std::path::{Path, PathBuf};
#[allow(dead_code)]
#[derive(Clone)]
pub struct SingleFormat {
pub identifier: String,
@ -167,18 +168,19 @@ impl SingleFormat {
pub async fn stream(&self) -> Result<Stream> {
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 stream.session.uses_stream_limits {
bail!("Found a stream which probably uses DRM. DRM downloads aren't supported")
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)
};
Ok(stream?)
}
pub async fn skip_events(&self) -> Result<Option<SkipEvents>> {
@ -346,6 +348,7 @@ impl Iterator for SingleFormatCollectionIterator {
}
}
#[allow(dead_code)]
#[derive(Clone)]
pub struct Format {
pub title: String,
@ -353,8 +356,6 @@ pub struct Format {
pub locales: Vec<(Locale, Vec<Locale>)>,
// deprecated
pub resolution: Resolution,
pub width: u64,
pub height: u64,
pub fps: f64,
@ -400,7 +401,6 @@ 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.resolution.width.to_string(), true, universal),
&sanitize(self.width.to_string(), true, universal),
)
.replace(
"{height}",
&sanitize(self.resolution.height.to_string(), true, universal),
&sanitize(self.height.to_string(), true, universal),
)
.replace("{series_id}", &sanitize(&self.series_id, true, universal))
.replace(
@ -588,7 +588,7 @@ impl Format {
.collect::<Vec<String>>()
.join(", ")
);
tab_info!("Resolution: {}", self.resolution);
tab_info!("Resolution: {}x{}", self.height, self.width);
tab_info!("FPS: {:.2}", self.fps)
}

View file

@ -5,28 +5,12 @@ use crunchyroll_rs::Locale;
pub async fn stream_data_from_stream(
stream: &Stream,
resolution: &Resolution,
subtitle: Option<Locale>,
hardsub_subtitle: Option<Locale>,
) -> Result<Option<(StreamData, StreamData, bool)>> {
// 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<Locale> = 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)
let (hardsub_locale, mut contains_hardsub) = if hardsub_subtitle.is_some() {
(hardsub_subtitle, true)
} 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 {
let hardsubs_requested = subtitle.is_some();
(subtitle, hardsubs_requested)
};
let (mut videos, mut audios) = match stream.stream_data(hardsub_locale).await {
@ -43,6 +27,11 @@ 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());