From fe50ecbb23eba788fc6838f35ebbdcd5373eb14f Mon Sep 17 00:00:00 2001 From: euzu Date: Tue, 11 Mar 2025 14:56:10 +0100 Subject: [PATCH] Replaced actix with axum --- Cargo.lock | 805 +++++------------- Cargo.toml | 14 +- src/api/api_utils.rs | 227 +++-- src/api/endpoints/download_api.rs | 31 +- src/api/endpoints/hdhomerun_api.rs | 100 ++- src/api/endpoints/hls_api.rs | 78 +- src/api/endpoints/m3u_api.rs | 140 +-- src/api/endpoints/user_api.rs | 113 ++- src/api/endpoints/v1_api.rs | 167 ++-- src/api/endpoints/web_index.rs | 82 +- src/api/endpoints/xmltv_api.rs | 77 +- src/api/endpoints/xtream_api.rs | 316 ++++--- src/api/main_api.rs | 212 +++-- src/api/model/active_provider_manager.rs | 77 +- src/api/model/active_user_manager.rs | 48 +- src/api/model/app_state.rs | 11 +- src/api/model/download.rs | 7 - src/api/model/model_utils.rs | 50 +- src/api/model/request.rs | 7 - src/api/model/stream_error.rs | 2 + src/api/model/streams/active_client_stream.rs | 19 +- src/api/model/streams/buffered_stream.rs | 4 +- src/api/model/streams/persist_pipe_stream.rs | 5 +- src/api/model/streams/provider_stream.rs | 10 +- .../model/streams/provider_stream_factory.rs | 21 +- .../model/streams/shared_stream_manager.rs | 56 +- src/api/model/xtream.rs | 8 +- src/api/scheduler.rs | 4 +- src/auth/authenticator.rs | 74 +- src/foundation/filter.rs | 47 +- src/main.rs | 34 +- src/messaging.rs | 4 +- src/model/api_proxy.rs | 4 +- src/model/config.rs | 41 +- src/model/healthcheck.rs | 4 +- src/model/mapping.rs | 24 +- src/model/playlist.rs | 189 ++-- src/model/xmltv.rs | 9 +- src/model/xtream.rs | 78 +- src/processing/parser/m3u.rs | 30 +- src/processing/parser/xmltv.rs | 13 +- src/processing/parser/xtream.rs | 98 +-- src/processing/playlist_watch.rs | 2 +- src/processing/processor/affix.rs | 2 +- src/processing/processor/playlist.rs | 94 +- src/processing/processor/xtream.rs | 8 +- src/processing/processor/xtream_series.rs | 28 +- src/processing/processor/xtream_vod.rs | 13 +- src/repository/kodi_repository.rs | 69 +- src/repository/m3u_playlist_iterator.rs | 4 +- src/repository/m3u_repository.rs | 6 +- src/repository/playlist_repository.rs | 12 +- src/repository/target_id_mapping.rs | 8 +- src/repository/xtream_playlist_iterator.rs | 4 +- src/repository/xtream_repository.rs | 62 +- src/tools/lru_cache.rs | 2 +- src/utils/file/config_reader.rs | 4 +- src/utils/file/file_lock_manager.rs | 36 +- src/utils/network/request.rs | 2 +- src/utils/network/xtream.rs | 12 +- 60 files changed, 1774 insertions(+), 1934 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c82f38b70..995d799c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,243 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.9.0", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9e772b3bcafe335042b5db010ab7c09013dad6eac4915c91d8d50902769f331" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-files" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0773d59061dedb49a8aed04c67291b9d8cf2fe0b60130a381aab53c6dd86e9be" -dependencies = [ - "actix-http", - "actix-service", - "actix-utils", - "actix-web", - "bitflags 2.9.0", - "bytes", - "derive_more", - "futures-core", - "http-range", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "v_htmlescape", -] - -[[package]] -name = "actix-http" -version = "3.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "ahash", - "base64 0.22.1", - "bitflags 2.9.0", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "futures-core", - "h2 0.3.26", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.8.5", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" -dependencies = [ - "actix-macros", - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" -dependencies = [ - "futures-core", - "paste", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "ahash", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2", - "time", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "actix-web-httpauth" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456348ed9dcd72a13a1f4a660449fafdecee9ac8205552e286809eb5b0b29bd3" -dependencies = [ - "actix-utils", - "actix-web", - "base64 0.22.1", - "futures-core", - "futures-util", - "log", - "pin-project-lite", -] - [[package]] name = "addr2line" version = "0.24.2" @@ -254,19 +17,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "getrandom 0.2.15", - "once_cell", - "version_check", - "zerocopy 0.7.35", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -276,21 +26,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -358,9 +93,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] name = "arrayref" @@ -397,6 +132,83 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.2.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-auth" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93495037c01c639b198ecb926b58f7f1c0d61ae663edcd61b2dd679f2a0bffe6" +dependencies = [ + "axum-core", + "base64 0.22.1", + "http 1.2.0", +] + +[[package]] +name = "axum-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -426,19 +238,20 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bincode" -version = "2.0.0-rc.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95" +checksum = "3ad1fa75f77bbd06f187540aa1d70ca50b80b27ce85e7f41c0ce7ff42b34ed3b" dependencies = [ "bincode_derive", "serde", + "unty", ] [[package]] name = "bincode_derive" -version = "2.0.0-rc.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c" +checksum = "b1cef5dd4a4457dd11529e743d18ba4fabbd5f20b6895f4c865cb257337dcf9f" dependencies = [ "virtue", ] @@ -488,53 +301,17 @@ dependencies = [ "generic-array", ] -[[package]] -name = "brotli" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "4.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bumpalo" version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" - -[[package]] -name = "bytestring" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e465647ae23b2823b0753f50decb2d5a86d2bb2cac04788fafd1f80e45378e5f" -dependencies = [ - "bytes", -] +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "castaway" @@ -548,8 +325,6 @@ version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -640,23 +415,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -824,19 +582,6 @@ dependencies = [ "syn", ] -[[package]] -name = "derive_more" -version = "0.99.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - [[package]] name = "digest" version = "0.10.7" @@ -1144,25 +889,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.8" @@ -1240,16 +966,16 @@ dependencies = [ ] [[package]] -name = "http-range" -version = "0.1.5" +name = "http-range-header" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" [[package]] name = "httparse" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1272,10 +998,11 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.8", + "h2", "http 1.2.0", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1504,12 +1231,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - [[package]] name = "indexmap" version = "2.7.1" @@ -1572,18 +1293,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" @@ -1610,12 +1322,6 @@ dependencies = [ "simple_asn1", ] -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - [[package]] name = "libc" version = "0.2.170" @@ -1657,9 +1363,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.15" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" [[package]] name = "litemap" @@ -1667,23 +1373,6 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - [[package]] name = "lock_api" version = "0.4.12" @@ -1704,11 +1393,8 @@ checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" name = "m3u-filter" version = "2.2.1" dependencies = [ - "actix-cors", - "actix-files", - "actix-rt", - "actix-web", - "actix-web-httpauth", + "axum", + "axum-auth", "bincode", "blake3", "bytes", @@ -1725,7 +1411,6 @@ dependencies = [ "log", "mime", "openssl", - "parking_lot", "paste", "path-clean", "pest", @@ -1741,16 +1426,24 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "socket2", "tempfile", "tokio", "tokio-stream", + "tokio-util", + "tower", + "tower-http", "unidecode", "url", "vergen", "winapi", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.4" @@ -1789,7 +1482,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -2027,18 +1719,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", @@ -2059,9 +1751,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polling" @@ -2087,18 +1779,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -2167,9 +1859,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ "proc-macro2", ] @@ -2192,8 +1884,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.2", - "zerocopy 0.8.21", + "rand_core 0.9.3", + "zerocopy", ] [[package]] @@ -2213,7 +1905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.2", + "rand_core 0.9.3", ] [[package]] @@ -2227,19 +1919,18 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a509b1a2ffbe92afab0e55c8fd99dea1c280e8171bd2d88682bb20bc41cbc2c" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom 0.3.1", - "zerocopy 0.8.21", ] [[package]] name = "redox_syscall" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ "bitflags 2.9.0", ] @@ -2267,12 +1958,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -2291,7 +1976,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.8", + "h2", "http 1.2.0", "http-body", "http-body-util", @@ -2333,9 +2018,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.11" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", @@ -2389,15 +2074,6 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - [[package]] name = "rustelebot" version = "0.3.2" @@ -2413,9 +2089,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" dependencies = [ "bitflags 2.9.0", "errno", @@ -2469,9 +2145,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ruzstd" @@ -2484,9 +2160,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "schannel" @@ -2526,26 +2202,20 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" - [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -2554,9 +2224,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.139" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -2564,6 +2234,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2589,17 +2269,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.8" @@ -2617,15 +2286,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "simple_asn1" version = "0.6.3" @@ -2694,9 +2354,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.98" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -2746,9 +2406,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.17.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" dependencies = [ "cfg-if", "fastrand 2.3.0", @@ -2760,18 +2420,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -2780,9 +2440,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa", @@ -2797,15 +2457,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", @@ -2823,9 +2483,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" dependencies = [ "tinyvec_macros", ] @@ -2838,9 +2498,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ "backtrace", "bytes", @@ -2848,11 +2508,22 @@ dependencies = [ "mio", "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" @@ -2865,9 +2536,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -2911,6 +2582,33 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +dependencies = [ + "base64 0.22.1", + "bitflags 2.9.0", + "bytes", + "futures-util", + "http 1.2.0", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", ] [[package]] @@ -2999,9 +2697,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unidecode" @@ -3021,6 +2719,12 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a88342087869553c259588a3ec9ca73ce9b2d538b7051ba5789ff236b6c129" + [[package]] name = "url" version = "2.5.4" @@ -3050,12 +2754,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "v_htmlescape" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" - [[package]] name = "vcpkg" version = "0.2.15" @@ -3094,9 +2792,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "virtue" -version = "0.0.13" +version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" [[package]] name = "waker-fn" @@ -3512,39 +3210,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf01143b2dd5d134f11f545cf9f1431b13b749695cb33bcce051e7568f99478" -dependencies = [ - "zerocopy-derive 0.8.21", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712c8386f4f4299382c9abee219bee7084f78fb939d88b6840fcc1320d5f6da2" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" dependencies = [ "proc-macro2", "quote", @@ -3599,31 +3276,3 @@ dependencies = [ "quote", "syn", ] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3051792fbdc2e1e143244dc28c60f73d8470e93f3f9cbd0ead44da5ed802722" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.14+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb060d4926e4ac3a3ad15d864e99ceb5f343c6b34f5bd6d81ae6ed417311be5" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml index 1af51a04c..3d737e143 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,11 +24,10 @@ url = "2.5" reqwest = { version = "0", features = ["blocking", "json", "stream", "rustls-tls"] } chrono = "0.4" cron = "0.15" -actix-web = "4.9" -actix-files = "0" -actix-cors = "0" -actix-rt = "2.10" -actix-web-httpauth = "0.8" +axum = { version = "0" , features = ["macros", "default"]} +axum-auth = "0" +tower = "0" +tower-http = { version = "0", features = ["cors", "auth", "fs"]} jsonwebtoken = "9.3" rust-argon2 = "2.1" futures = "0.3" @@ -49,13 +48,12 @@ flate2 = "1" blake3 = "1.5" bytes = "1.10" tokio-stream = { version = "0.1", features = ["sync"] } -tokio = "1.43" +tokio = { version = "1.43", features = ["rt-multi-thread", "parking_lot", "fs"] } +tokio-util = "0.7" paste = "1.0" tempfile = "3.16" ruzstd = "0" filetime = "0.2" -parking_lot = "0.12" -socket2 = "0.5" #[cfg(target_os = "macos")] libc = "0" #[cfg(target_os = "windows")] diff --git a/src/api/api_utils.rs b/src/api/api_utils.rs index 45a691f96..ff77faa31 100644 --- a/src/api/api_utils.rs +++ b/src/api/api_utils.rs @@ -13,19 +13,16 @@ use crate::utils::file::file_utils::{create_new_file_for_write}; use crate::tools::lru_cache::LRUResourceCache; use crate::utils::network::request; use crate::utils::network::request::sanitize_sensitive_info; -use actix_files::NamedFile; -use actix_web::body::{BodyStream, SizedStream}; -use actix_web::http::header::{HeaderValue, CACHE_CONTROL}; -use actix_web::{web, HttpRequest, HttpResponse}; -use parking_lot::Mutex; -use futures::TryStreamExt; +use futures::{StreamExt, TryStreamExt}; use log::{error, log_enabled, trace}; use reqwest::StatusCode; use std::collections::HashMap; use std::io::BufWriter; use std::path::Path; -use std::sync::Arc; -use actix_web_httpauth::extractors::bearer::BearerAuth; +use std::sync::{Arc}; +use tokio::sync::Mutex; +use axum::http::HeaderMap; +use axum::response::IntoResponse; use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use url::Url; use crate::api::model::streams::active_client_stream::ActiveClientStream; @@ -38,14 +35,14 @@ macro_rules! try_option_bad_request { Some(value) => value, None => { if $msg_is_error {error!("{}", $msg);} else {debug!("{}", $msg);} - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } } }; ($option:expr) => { match $option { Some(value) => value, - None => return HttpResponse::BadRequest().finish(), + None => return axum::http::StatusCode::BAD_REQUEST.into_response(), } }; } @@ -57,14 +54,14 @@ macro_rules! try_result_bad_request { Ok(value) => value, Err(_) => { if $msg_is_error {error!("{}", $msg);} else {debug!("{}", $msg);} - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } } }; ($option:expr) => { match $option { Ok(value) => value, - Err(_) => return HttpResponse::BadRequest().finish(), + Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response(), } }; } @@ -73,22 +70,32 @@ pub use try_option_bad_request; pub use try_result_bad_request; use crate::auth::authenticator::Claims; -pub async fn serve_file(file_path: &Path, req: &HttpRequest, mime_type: mime::Mime) -> HttpResponse { +pub async fn serve_file(file_path: &Path, mime_type: mime::Mime) -> impl axum::response::IntoResponse + Send { if file_path.exists() { - if let Ok(file) = actix_files::NamedFile::open_async(file_path).await { - let mut result = file.set_content_type(mime_type) - .disable_content_disposition().into_response(req); - let headers = result.headers_mut(); - headers.insert(CACHE_CONTROL, HeaderValue::from_bytes(b"no-cache").unwrap()); - return result; - } + return match tokio::fs::File::open(file_path).await { + Ok(file) => { + let reader = tokio::io::BufReader::new(file); + let stream = tokio_util::io::ReaderStream::new(reader); + let body = axum::body::Body::from_stream(stream); + + axum::response::Response::builder() + .status(StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime_type.to_string()) + .header(axum::http::header::CACHE_CONTROL, axum::http::header::HeaderValue::from_static("no-cache")) + .body(body) + .unwrap() + .into_response() + } + Err(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(), + }; } - HttpResponse::NoContent().finish() + axum::http::StatusCode::NOT_FOUND.into_response() + } pub async fn get_user_target_by_username<'a>(username: &str, app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> { if !username.is_empty() { - return app_state.config.get_target_for_username(username); + return app_state.config.get_target_for_username(username).await; } None } @@ -96,13 +103,13 @@ pub async fn get_user_target_by_username<'a>(username: &str, app_state: &'a AppS pub async fn get_user_target_by_credentials<'a>(username: &str, password: &str, api_req: &'a UserApiRequest, app_state: &'a AppState) -> Option<(ProxyUserCredentials, &'a ConfigTarget)> { if !username.is_empty() && !password.is_empty() { - app_state.config.get_target_for_user(username, password) + app_state.config.get_target_for_user(username, password).await } else { let token = api_req.token.as_str().trim(); if token.is_empty() { None } else { - app_state.config.get_target_for_user_by_token(token) + app_state.config.get_target_for_user_by_token(token).await } } } @@ -130,26 +137,27 @@ fn get_stream_options(app_state: &AppState) -> (bool, bool, usize, bool) { (stream_retry, buffer_enabled, buffer_size, pipe_provider_stream) } -fn get_stream_content_length(provider_response: Option<&(Vec<(String, String)>, StatusCode)>) -> u64 { - let content_length = provider_response - .as_ref() - .and_then(|(headers, _)| headers.iter().find(|(h, _)| h.eq(actix_web::http::header::CONTENT_LENGTH.as_str()))) - .and_then(|(_, val)| val.parse::().ok()) - .unwrap_or(0); - content_length -} +// fn get_stream_content_length(provider_response: Option<&(Vec<(String, String)>, StatusCode)>) -> u64 { +// let content_length = provider_response +// .as_ref() +// .and_then(|(headers, _)| headers.iter().find(|(h, _)| h.eq(axum::http::header::CONTENT_LENGTH.as_str()))) +// .and_then(|(_, val)| val.parse::().ok()) +// .unwrap_or(0); +// content_length +// } pub async fn stream_response(app_state: &AppState, stream_url: &str, - req: &HttpRequest, input: Option<&ConfigInput>, + req_headers: &HeaderMap, + input: Option<&ConfigInput>, item_type: PlaylistItemType, target: &ConfigTarget, - user: &ProxyUserCredentials) -> HttpResponse { + user: &ProxyUserCredentials) -> impl axum::response::IntoResponse + Send { if log_enabled!(log::Level::Trace) { trace!("Try to open stream {}", sanitize_sensitive_info(stream_url)); } let log_active_clients = app_state.config.log.as_ref().is_some_and(|l| l.active_clients); let share_stream = is_stream_share_enabled(item_type, target); if share_stream { - if let Some(value) = shared_stream_response(app_state, stream_url, log_active_clients, user) { - return value; + if let Some(value) = shared_stream_response(app_state, stream_url, log_active_clients, user).await { + return value.into_response(); } } @@ -159,47 +167,65 @@ pub async fn stream_response(app_state: &AppState, stream_url: &str, if let Ok(url) = Url::parse(stream_url) { let active_clients = Arc::clone(&app_state.active_users); let (stream_opt, provider_response) = if direct_pipe_provider_stream { - provider_stream::get_provider_pipe_stream(&app_state.config, &app_state.http_client, &url, req, input, item_type).await + provider_stream::get_provider_pipe_stream(&app_state.config, &app_state.http_client, &url, req_headers, input, item_type).await } else { let buffer_stream_options = BufferStreamOptions::new(item_type, stream_retry, buffer_enabled, buffer_size, share_stream); - provider_stream::get_provider_reconnect_buffered_stream(&app_state.config, &app_state.http_client, &url, req, input, buffer_stream_options).await + provider_stream::get_provider_reconnect_buffered_stream(&app_state.config, &app_state.http_client, &url, req_headers, input, buffer_stream_options).await }; if let Some(stream) = stream_opt { - let content_length = get_stream_content_length(provider_response.as_ref()); - let stream = ActiveClientStream::new(stream, active_clients, user, log_active_clients); + // let content_length = get_stream_content_length(provider_response.as_ref()); + let stream = ActiveClientStream::new(stream, active_clients, user, log_active_clients).await; let stream_resp = if share_stream { let shared_headers = provider_response.as_ref().map_or_else(Vec::new, |(h, _)| h.clone()); - SharedStreamManager::subscribe(app_state, stream_url, stream, shared_headers, buffer_size); - if let Some(broadcast_stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url) { - let mut response_builder = get_stream_response_with_headers(provider_response, stream_url); - if content_length > 0 { - response_builder.body(SizedStream::new(content_length, broadcast_stream)) } - else { - response_builder.body(BodyStream::new(broadcast_stream)) + SharedStreamManager::subscribe(app_state, stream_url, stream, shared_headers, buffer_size).await; + if let Some(broadcast_stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url).await { + let (status_code, header_map) = get_stream_response_with_headers(provider_response, stream_url); + let mut response = axum::response::Response::builder() + .status(status_code); + for (key, value) in &header_map { + response = response.header(key, value); } + response.body(axum::body::Body::from_stream(broadcast_stream)).unwrap().into_response() + // if content_length > 0 { + // response_builder.body(SizedStream::new(content_length, broadcast_stream)) } + // else { + // response_builder.body(BodyStream::new(broadcast_stream)) + // } } else { - HttpResponse::BadRequest().finish() + axum::http::StatusCode::BAD_REQUEST.into_response() } } else { - let mut response_builder = get_stream_response_with_headers(provider_response, stream_url); - if content_length > 0 { response_builder.body(SizedStream::new(content_length, stream)) } else { response_builder.streaming(stream) } + let (status_code, header_map) = get_stream_response_with_headers(provider_response, stream_url); + let mut response = axum::response::Response::builder() + .status(status_code); + for (key, value) in &header_map { + response = response.header(key, value); + } + response.body(axum::body::Body::from_stream(stream)).unwrap().into_response() + + // if content_length > 0 { response_builder.body(SizedStream::new(content_length, stream)) } else { response_builder.streaming(stream) } }; - return stream_resp; + return stream_resp.into_response(); } } error!("Cant open stream {}", sanitize_sensitive_info(stream_url)); - HttpResponse::BadRequest().finish() + axum::http::StatusCode::BAD_REQUEST.into_response() } -fn shared_stream_response(app_state: &AppState, stream_url: &str, log_active_clients: bool, user: &ProxyUserCredentials) -> Option { - if let Some(stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url) { +async fn shared_stream_response(app_state: &AppState, stream_url: &str, log_active_clients: bool, user: &ProxyUserCredentials) -> Option { + if let Some(stream) = SharedStreamManager::subscribe_shared_stream(app_state, stream_url).await { debug_if_enabled!("Using shared channel {}", sanitize_sensitive_info(stream_url)); - if let Some(headers) = app_state.shared_stream_manager.get_shared_state_headers(stream_url) { - let mut response_builder = get_stream_response_with_headers(Some((headers.clone(), StatusCode::OK)), stream_url); + if let Some(headers) = app_state.shared_stream_manager.get_shared_state_headers(stream_url).await { + let (status_code, header_map) = get_stream_response_with_headers(Some((headers.clone(), StatusCode::OK)), stream_url); let active_clients = Arc::clone(&app_state.active_users); - let stream = ActiveClientStream::new(stream, active_clients, user, log_active_clients); - return Some(response_builder.body(BodyStream::new(stream))); + let stream = ActiveClientStream::new(stream, active_clients, user, log_active_clients).await.boxed(); + let mut response = axum::response::Response::builder() + .status(status_code); + for (key, value) in &header_map { + response = response.header(key, value); + } + return Some(response.body(axum::body::Body::from_stream(stream)).unwrap()); } } None @@ -209,9 +235,9 @@ pub fn is_stream_share_enabled(item_type: PlaylistItemType, target: &ConfigTarge (item_type == PlaylistItemType::Live || item_type == PlaylistItemType::LiveHls) && target.options.as_ref().is_some_and(|opt| opt.share_live_streams) } -pub type HeaderFilter = Option bool>>; -pub fn get_headers_from_request(req: &HttpRequest, filter: &HeaderFilter) -> HashMap> { - req.headers() +pub type HeaderFilter = Option bool + Send>>; +pub fn get_headers_from_request(req_headers: &HeaderMap, filter: &HeaderFilter) -> HashMap> { + req_headers .iter() .filter(|(k, _)| match &filter { None => true, @@ -221,36 +247,34 @@ pub fn get_headers_from_request(req: &HttpRequest, filter: &HeaderFilter) -> Has .collect() } -fn get_add_cache_content(res_url: &str, cache: &Arc>>) -> Box { +fn get_add_cache_content(res_url: &str, cache: &Arc>>) -> Arc { let resource_url = String::from(res_url); let cache = Arc::clone(cache); - let add_cache_content: Box = Box::new(move |size| { + let add_cache_content: Arc = Arc::new(move |size| { let res_url = resource_url.clone(); let cache = Arc::clone(&cache); - actix_rt::spawn(async move { + tokio::spawn(async move { if let Some(cache) = cache.as_ref() { - let mut guard = cache.lock(); - let _ = guard.add_content(&res_url, size); + let _ = cache.lock().await.add_content(&res_url, size); } }); }); add_cache_content } -pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &HttpRequest, input: Option<&ConfigInput>) -> HttpResponse { +pub async fn resource_response(app_state: &AppState, resource_url: &str, req_headers: &HeaderMap, input: Option<&ConfigInput>) -> impl axum::response::IntoResponse + Send { if resource_url.is_empty() { - return HttpResponse::NoContent().finish(); + return axum::http::StatusCode::NO_CONTENT.into_response(); } let filter: HeaderFilter = Some(Box::new(|key| key != "if-none-match" && key != "if-modified-since")); - let req_headers = get_headers_from_request(req, &filter); + let req_headers = get_headers_from_request(req_headers, &filter); if let Some(cache) = app_state.cache.as_ref() { - let mut guard = cache.lock(); + let mut guard = cache.lock().await; if let Some(resource_path) = guard.get_content(resource_url) { - if let Ok(named_file) = NamedFile::open(resource_path) { - trace_if_enabled!("Responding resource from cache {}", sanitize_sensitive_info(resource_url)); - return named_file.into_response(req); - } + trace_if_enabled!("Responding resource from cache {}", sanitize_sensitive_info(resource_url)); + return serve_file(&resource_path, mime::APPLICATION_OCTET_STREAM).await.into_response(); } + } trace_if_enabled!("Try to fetch resource {}", sanitize_sensitive_info(resource_url)); if let Ok(url) = Url::parse(resource_url) { @@ -259,24 +283,23 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &H Ok(response) => { let status = response.status(); if status.is_success() { - let mut response_builder = HttpResponse::Ok(); - response.headers().iter().for_each(|(k, v)| { - response_builder.insert_header((k.as_str(), v.as_ref())); - }); + let mut response_builder = axum::response::Response::builder() + .status(StatusCode::OK); + for (key, value) in response.headers() { + response_builder = response_builder.header(key, value); + } let byte_stream = response.bytes_stream().map_err(|err| StreamError::reqwest(&err)); if let Some(cache) = app_state.cache.as_ref() { - let resource_path = { - cache.lock().store_path(resource_url) - }; + let resource_path = cache.lock().await.store_path(resource_url); if let Ok(file) = create_new_file_for_write(&resource_path) { let writer = BufWriter::new(file); let add_cache_content = get_add_cache_content(resource_url, &app_state.cache); let stream = PersistPipeStream::new(byte_stream, writer, add_cache_content); - return response_builder.body(BodyStream::new(stream)); + return response_builder.body(axum::body::Body::from_stream(stream)).unwrap().into_response(); } } - return response_builder.body(BodyStream::new(byte_stream)); + return response_builder.body(axum::body::Body::from_stream(byte_stream)).unwrap().into_response(); } debug_if_enabled!("Failed to open resource got status {} for {}", status, sanitize_sensitive_info(resource_url)); } @@ -287,7 +310,7 @@ pub async fn resource_response(app_state: &AppState, resource_url: &str, req: &H } else { error!("Url is malformed {}", sanitize_sensitive_info(resource_url)); } - HttpResponse::BadRequest().finish() + axum::http::StatusCode::BAD_REQUEST.into_response() } pub fn separate_number_and_remainder(input: &str) -> (String, Option) { @@ -298,18 +321,36 @@ pub fn separate_number_and_remainder(input: &str) -> (String, Option) { }) } -pub fn empty_json_list_response() -> HttpResponse { - HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("[]") +pub fn empty_json_list_response() -> impl axum::response::IntoResponse + Send { + axum::response::Response::builder() + .status(StatusCode::OK) + .header("Content-Type", mime::APPLICATION_JSON.to_string()) + .body("[]".to_string()) + .unwrap() + .into_response() } -pub fn get_username_from_auth_header(credentials: Option, app_state: &web::Data>) -> Option { - if let Some(bearer) = credentials { - if let Some(web_auth_config) = app_state.config.web_auth.as_ref() { - let secret_key = web_auth_config.secret.as_ref(); - if let Ok(token_data) = decode::(bearer.token(), &DecodingKey::from_secret(secret_key), &Validation::new(Algorithm::HS256)) { - return Some(token_data.claims.username); - } +pub fn get_username_from_auth_header( + token: &str, + app_state: &Arc, +) -> Option { + if let Some(web_auth_config) = &app_state.config.web_auth { + let secret_key: &str = web_auth_config.secret.as_ref(); + if let Ok(token_data) = decode::( + token, + &DecodingKey::from_secret(secret_key.as_bytes()), + &Validation::new(Algorithm::HS256), + ) { + return Some(token_data.claims.username); } } None } + +pub fn redirect(url: &str) -> impl IntoResponse { + axum::response::Response::builder() + .status(StatusCode::FOUND) + .header("Location", url) + .body(axum::body::Body::empty()) + .unwrap() +} \ No newline at end of file diff --git a/src/api/endpoints/download_api.rs b/src/api/endpoints/download_api.rs index f70d7020f..e31e8c8ec 100644 --- a/src/api/endpoints/download_api.rs +++ b/src/api/endpoints/download_api.rs @@ -2,7 +2,6 @@ use crate::api::model::app_state::AppState; use crate::api::model::download::{DownloadQueue, FileDownload, FileDownloadRequest}; use crate::model::config::VideoDownloadConfig; use crate::utils::network::request; -use actix_web::{web, HttpResponse}; use tokio::sync::RwLock; use futures::stream::TryStreamExt; use log::info; @@ -12,6 +11,7 @@ use std::io::{Write}; use std::ops::Deref; use std::sync::Arc; use std::{fs}; +use axum::response::IntoResponse; use crate::m3u_filter_error::to_io_error; async fn download_file(active: Arc>>, client: &reqwest::Client) -> Result<(), String> { @@ -69,7 +69,7 @@ async fn run_download_queue(download_cfg: &VideoDownloadConfig, download_queue: let dq = Arc::clone(download_queue); match reqwest::Client::builder().default_headers(headers).build() { Ok(client) => { - actix_rt::spawn(async move { + tokio::spawn(async move { loop { if dq.active.read().await.deref().is_some() { match download_file(Arc::clone(&dq.active), &client).await { @@ -110,41 +110,40 @@ macro_rules! download_info { } pub async fn queue_download_file( - req: web::Json, - app_state: web::Data>, -) -> HttpResponse { + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Json(req): axum::extract::Json, +) -> impl axum::response::IntoResponse + Send { if let Some(download_cfg) = &app_state.config.video.as_ref().unwrap().download { if download_cfg.directory.is_none() { - return HttpResponse::BadRequest().json(json!({"error": "Server config missing video.download.directory configuration"})); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download.directory configuration"}))).into_response(); } match FileDownload::new(req.url.as_str(), req.filename.as_str(), download_cfg) { Some(file_download) => { - let response = HttpResponse::Ok().json(download_info!(file_download)); - app_state.downloads.queue.lock().await.push_back(file_download); + app_state.downloads.queue.lock().await.push_back(file_download.clone()); if app_state.downloads.active.read().await.is_none() { match run_download_queue(download_cfg, &app_state.downloads).await { Ok(()) => {} - Err(err) => return HttpResponse::InternalServerError().json(json!({"error": err})), + Err(err) => return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err}))).into_response(), } } - response + axum::Json(download_info!(&file_download)).into_response() } - None => HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})), + None => (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response(), } } else { - HttpResponse::BadRequest().json(json!({"error": "Server config missing video.download configuration"})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Server config missing video.download configuration"}))).into_response() } } pub async fn download_file_info( - app_state: web::Data>, -) -> HttpResponse { + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { let finished_list: &[Value] = &app_state.downloads.finished.write().await.drain(..) .map(|fd| download_info!(fd)).collect::>(); - (*app_state.downloads.active.read().await).as_ref().map_or_else(|| HttpResponse::Ok().json(json!({ + (*app_state.downloads.active.read().await).as_ref().map_or_else(|| axum::Json(json!({ "completed": true, "downloads": finished_list - })), |file_download| HttpResponse::Ok().json(json!({ + })), |file_download| axum::Json(json!({ "completed": false, "downloads": finished_list, "active": download_info!(file_download) }))) } \ No newline at end of file diff --git a/src/api/endpoints/hdhomerun_api.rs b/src/api/endpoints/hdhomerun_api.rs index 14be4cd4f..6ec0ecdc2 100644 --- a/src/api/endpoints/hdhomerun_api.rs +++ b/src/api/endpoints/hdhomerun_api.rs @@ -1,12 +1,11 @@ use std::sync::Arc; -use crate::api::model::app_state::HdHomerunAppState; +use axum::response::IntoResponse; +use crate::api::model::app_state::{HdHomerunAppState}; use crate::model::api_proxy::{ProxyType, ProxyUserCredentials}; use crate::model::config::{Config, TargetType}; use crate::model::playlist::{M3uPlaylistItem, XtreamCluster, XtreamPlaylistItem}; use crate::processing::parser::xtream::get_xtream_url; use crate::utils::json_utils::get_string_from_serde_value; -// https://info.hdhomerun.com/info/http_api -use actix_web::{web, HttpResponse, Responder}; use bytes::Bytes; use futures::{stream, Stream, StreamExt}; use log::{error, warn}; @@ -14,6 +13,9 @@ use serde::{Deserialize, Serialize}; use serde_json::{json}; use crate::repository::m3u_playlist_iterator::M3uPlaylistIterator; use crate::repository::xtream_playlist_iterator::{XtreamPlaylistIterator}; + +// https://info.hdhomerun.com/info/http_api + // const DISCOVERY_BYTES: &[u8] = &[0, 2, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143]; // const RESPONSE_BYTES: &[u8] = &[0, 3, 0, 12, 1, 4, 255, 255, 255, 255, 2, 4, 255, 255, 255, 255, 115, 204, 125, 143]; @@ -154,9 +156,9 @@ where } } -fn create_device(app_state: &web::Data) -> Option { - if let Some(credentials) = app_state.app_state.config.get_user_credentials(&app_state.device.t_username) { - let server_info = app_state.app_state.config.get_user_server_info(&credentials); +async fn create_device(app_state: &Arc) -> Option { + if let Some(credentials) = app_state.app_state.config.get_user_credentials(&app_state.device.t_username).await { + let server_info = app_state.app_state.config.get_user_server_info(&credentials).await; let device = &app_state.device; let device_url = format!("{}://{}:{}", server_info.protocol, server_info.host, device.port); Some(Device { @@ -180,36 +182,37 @@ fn create_device(app_state: &web::Data) -> Option { } } -async fn device_xml(app_state: web::Data) -> impl Responder { - if let Some(device) = create_device(&app_state) { - HttpResponse::Ok().content_type("application/xml").body(device.as_xml()) +async fn device_xml(axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { + if let Some(device) = create_device(&app_state).await { + axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, "application/xml") + .body(axum::body::Body::from(device.as_xml())) + .unwrap() + .into_response() } else { - HttpResponse::InternalServerError().finish() + axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response() } } -async fn device_json(app_state: web::Data) -> impl Responder { - if let Some(device) = create_device(&app_state) { - HttpResponse::Ok().json(device) +async fn device_json(axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { + if let Some(device) = create_device(&app_state).await { + axum::Json(device).into_response() } else { - HttpResponse::InternalServerError().finish() + axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response() } } -async fn discover_json(app_state: web::Data) -> impl Responder { - if let Some(device) = create_device(&app_state) { - HttpResponse::Ok() - .content_type("application/json") - .json(device) +async fn discover_json(axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { + if let Some(device) = create_device(&app_state).await { + axum::Json(device).into_response() } else { - HttpResponse::InternalServerError().finish() + axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response() } } -async fn lineup_status() -> impl Responder { - HttpResponse::Ok() - .content_type("application/json") - .json(json!({ +async fn lineup_status() -> impl IntoResponse { + axum::Json(json!({ "ScanInProgress": 0, "ScanPossible": 0, "Source": "Cable", @@ -217,9 +220,10 @@ async fn lineup_status() -> impl Responder { })) } -async fn lineup_json(app_state: web::Data) -> impl Responder { +#[axum::debug_handler] +async fn lineup_json(axum::extract::State(app_state): axum::extract::State>) -> impl IntoResponse { let cfg = Arc::clone(&app_state.app_state.config); - if let Some((credentials, target)) = cfg.get_target_for_username(&app_state.device.t_username) { + if let Some((credentials, target)) = cfg.get_target_for_username(&app_state.device.t_username).await { let use_output = target.get_hdhomerun_output().as_ref().and_then(|o| o.use_output.clone()); let use_all = use_output.is_none(); let use_m3u = use_output.as_ref() == Some(&TargetType::M3u); @@ -230,11 +234,13 @@ async fn lineup_json(app_state: web::Data) -> impl Responder let body_stream = stream::once(async { Ok(Bytes::from("[")) }) .chain(stream) .chain(stream::once(async { Ok(Bytes::from("]")) })); - return HttpResponse::Ok() - .content_type("application/json") - .streaming(body_stream); + return axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from_stream(body_stream)) + .unwrap().into_response(); } else if (use_all || use_xtream) && target.has_output(&TargetType::Xtream) { - let server_info = app_state.app_state.config.get_user_server_info(&credentials); + let server_info = app_state.app_state.config.get_user_server_info(&credentials).await; let base_url = if credentials.proxy == ProxyType::Reverse { Some(server_info.get_base_url()) } else { @@ -253,30 +259,34 @@ async fn lineup_json(app_state: web::Data) -> impl Responder .chain(stream::once(async { Ok(Bytes::from(",")) })) .chain(vod_stream) .chain(stream::once(async { Ok(Bytes::from("]")) })); - return HttpResponse::Ok() - .content_type("application/json") - .streaming(body_stream); + return axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from_stream(body_stream)) + .unwrap() + .into_response(); } } - HttpResponse::NotFound().finish() + axum::http::StatusCode::NOT_FOUND.into_response() } -async fn auto_channel(_app_state: web::Data, path: web::Path) -> impl Responder { - let channel = path.into_inner(); +async fn auto_channel(axum::extract::State(_app_state): axum::extract::State>, + axum::extract::Path(channel): axum::extract::Path) -> impl IntoResponse { warn!("HdHomerun api not implemented for auto_channel {channel}"); - HttpResponse::NotFound().finish() + axum::http::StatusCode::NOT_FOUND.into_response() } -pub fn hdhr_api_register(cfg: &mut web::ServiceConfig) { - cfg.service(web::resource("/device.xml").route(web::get().to(device_xml))); - cfg.service(web::resource("/device.json").route(web::get().to(device_json))); - cfg.service(web::resource("/discover.json").route(web::get().to(discover_json))); - cfg.service(web::resource("/lineup_status.json").route(web::get().to(lineup_status))); - cfg.service(web::resource("/lineup.json").route(web::get().to(lineup_json))); +pub fn hdhr_api_register() -> axum::Router> { + axum::Router::new() + .route("/device.xml", axum::routing::get(device_xml)) + .route("/device.json", axum::routing::get(device_json)) + .route("/discover.json", axum::routing::get(discover_json)) + .route("/lineup_status.json", axum::routing::get(lineup_status)) + .route("/lineup.json", axum::routing::get(lineup_json)) // cfg.service(web::resource("/lineup.xml").route(web::get().to(lineup_xml))); // cfg.service(web::resource("/lineup.m3u").route(web::get().to(lineup_m3u))); - cfg.service(web::resource("/auto/{channel}").route(web::get().to(auto_channel))); - cfg.service(web::resource("/tuner{tuner_num}/{channel}").route(web::get().to(auto_channel))); + .route("/auto/{channel}", axum::routing::get(auto_channel)) + .route("/tuner{tuner_num}/{channel}", axum::routing::get(auto_channel)) } // fn start_hdhomerum_discovery_handler(ssdp_socket: Arc, server: String, location: String, cache_control: String, usn: String) { diff --git a/src/api/endpoints/hls_api.rs b/src/api/endpoints/hls_api.rs index ad42327c1..aa2e26f1b 100644 --- a/src/api/endpoints/hls_api.rs +++ b/src/api/endpoints/hls_api.rs @@ -1,20 +1,19 @@ -use std::sync::Arc; -use actix_web::{web, HttpRequest, HttpResponse}; -use actix_web::web::Data; -use log::{debug, error}; -use serde::Deserialize; use crate::api::api_utils::{get_user_target_by_credentials, stream_response}; +use crate::api::api_utils::{try_option_bad_request, try_result_bad_request}; use crate::api::model::app_state::AppState; use crate::api::model::request::UserApiRequest; use crate::model::api_proxy::ProxyUserCredentials; use crate::model::config::{ConfigInput, TargetType}; use crate::model::playlist::{PlaylistEntry, PlaylistItemType, XtreamCluster}; use crate::processing::parser::hls::{rewrite_hls, M3U_HLSR_PREFIX}; -use crate::api::api_utils::{try_option_bad_request, try_result_bad_request}; -use crate::repository::{m3u_repository, xtream_repository}; use crate::repository::playlist_repository::HLS_EXT; +use crate::repository::{m3u_repository, xtream_repository}; use crate::utils::network::request; use crate::utils::network::request::{replace_extension, sanitize_sensitive_info}; +use axum::response::IntoResponse; +use log::{debug, error}; +use serde::Deserialize; +use std::sync::Arc; #[derive(Deserialize)] #[allow(dead_code)] @@ -27,40 +26,44 @@ struct HlsApiPathParams { chunk: String, } -pub(in crate::api) async fn handle_hls_stream_request(app_state: &Data>, user: &ProxyUserCredentials, pli: &dyn PlaylistEntry, input: &ConfigInput, target_type: TargetType) -> HttpResponse { +pub(in crate::api) async fn handle_hls_stream_request(app_state: &Arc, user: &ProxyUserCredentials, pli: &dyn PlaylistEntry, input: &ConfigInput, target_type: TargetType) -> impl axum::response::IntoResponse + Send { let url = replace_extension(&pli.get_provider_url(), HLS_EXT); match request::download_text_content(Arc::clone(&app_state.http_client), input, &url, None).await { Ok(content) => { let hls_content = rewrite_hls(&content, pli.get_virtual_id(), user, &target_type); - HttpResponse::Ok().content_type("application/x-mpegurl").body(hls_content) + axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, "application/x-mpegurl") + .body(hls_content) + .unwrap() + .into_response() } Err(err) => { error!("Failed to download m3u8 {}", sanitize_sensitive_info(err.to_string().as_str())); - HttpResponse::NoContent().finish() + axum::http::StatusCode::NO_CONTENT.into_response() } } } async fn hls_api_stream( - req: &HttpRequest, - api_req: &web::Query, - path: web::Path, - app_state: &web::Data>, - target_type: TargetType -) -> HttpResponse { - let params = path.into_inner(); + req_headers: &axum::http::HeaderMap, + api_req: &UserApiRequest, + params: HlsApiPathParams, + app_state: &Arc, + target_type: TargetType, +) -> impl axum::response::IntoResponse + Send { let (user, target) = try_option_bad_request!( get_user_target_by_credentials(¶ms.username, ¶ms.password, api_req, app_state).await, false, format!("Could not find any user {}", params.username)); - if !user.has_permissions(app_state) { - return HttpResponse::Forbidden().finish(); + if !user.has_permissions(app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } let target_name = &target.name; let virtual_id: u32 = try_result_bad_request!(params.channel.parse()); let (pli_url, input_name) = if target_type == TargetType::Xtream { - let (pli, _ ) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None), true, format!("Failed to read xtream item for stream id {}", virtual_id)); + let (pli, _) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None), true, format!("Failed to read xtream item for stream id {}", virtual_id)); (pli.url, pli.input_name) } else { let pli = try_result_bad_request!(m3u_repository::m3u_get_item_for_stream_id(virtual_id, &app_state.config, target).await, true, format!("Failed to read xtream item for stream id {}", virtual_id)); @@ -74,30 +77,33 @@ async fn hls_api_stream( // we don't respond as hlsr, we take the original stream, because the location could be different and then it does not work // The next problem is, different url to same channel causes to fail stream share. // let stream_url = format!("{input_url}/hlsr/{token}/{input_username}/{input_password}/{}/{hash}/{chunk}", pli.provider_id); - stream_response(app_state, &pli_url, req, Some(input), PlaylistItemType::Live, target, &user).await + stream_response(app_state, &pli_url, req_headers, Some(input), PlaylistItemType::Live, target, &user).await.into_response() } +#[axum::debug_handler] async fn hls_api_stream_xtream( - req: HttpRequest, - api_req: web::Query, - path: web::Path, - app_state: web::Data>, -) -> HttpResponse { - hls_api_stream(&req, &api_req, path, &app_state, TargetType::Xtream).await + req_headers: axum::http::HeaderMap, + axum::extract::Query(api_req): axum::extract::Query, + axum::extract::Path(params): axum::extract::Path, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { + hls_api_stream(&req_headers, &api_req, params, &app_state, TargetType::Xtream).await.into_response() } +#[axum::debug_handler] async fn hls_api_stream_m3u( - req: HttpRequest, - api_req: web::Query, - path: web::Path, - app_state: web::Data>, -) -> HttpResponse { - hls_api_stream(&req, &api_req, path, &app_state, TargetType::M3u).await + req_headers: axum::http::HeaderMap, + axum::extract::Query(api_req): axum::extract::Query, + axum::extract::Path(params): axum::extract::Path, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { + hls_api_stream(&req_headers, &api_req, params, &app_state, TargetType::M3u).await.into_response() } -pub fn hls_api_register(cfg: &mut web::ServiceConfig) { - cfg.service(web::resource("/hlsr/{token}/{username}/{password}/{channel}/{hash}/{chunk}").route(web::get().to(hls_api_stream_xtream))); - cfg.service(web::resource(format!("/{M3U_HLSR_PREFIX}/{{token}}/{{username}}/{{password}}/{{channel}}/{{hash}}/{{chunk}}")).route(web::get().to(hls_api_stream_m3u))); +pub fn hls_api_register() -> axum::Router> { + axum::Router::new() + .route("/hlsr/{token}/{username}/{password}/{channel}/{hash}/{chunk}", axum::routing::get(hls_api_stream_xtream)) + .route(&format!("/{M3U_HLSR_PREFIX}/{{token}}/{{username}}/{{password}}/{{channel}}/{{hash}}/{{chunk}}"), axum::routing::get(hls_api_stream_m3u)) //cfg.service(web::resource("/hls/{token}/{stream}").route(web::get().to(xtream_player_api_hls_stream))); //cfg.service(web::resource("/play/{token}/{type}").route(web::get().to(xtream_player_api_play_stream))); } \ No newline at end of file diff --git a/src/api/endpoints/m3u_api.rs b/src/api/endpoints/m3u_api.rs index 933d3382b..223acc1b2 100644 --- a/src/api/endpoints/m3u_api.rs +++ b/src/api/endpoints/m3u_api.rs @@ -1,12 +1,4 @@ -use std::sync::Arc; -use actix_web::{web, HttpRequest, HttpResponse}; -use bytes::Bytes; -use futures::stream; -use log::{debug, error}; - -use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, resource_response, - separate_number_and_remainder, stream_response, try_option_bad_request, - try_result_bad_request}; +use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, redirect, resource_response, separate_number_and_remainder, stream_response, try_option_bad_request, try_result_bad_request}; use crate::api::endpoints::hls_api::handle_hls_stream_request; use crate::api::model::app_state::AppState; use crate::api::model::request::UserApiRequest; @@ -15,73 +7,84 @@ use crate::model::config::TargetType; use crate::model::playlist::{FieldGetAccessor, XtreamCluster}; use crate::repository::m3u_playlist_iterator::{M3U_RESOURCE_PATH, M3U_STREAM_PATH}; use crate::repository::m3u_repository::{m3u_get_item_for_stream_id, m3u_load_rewrite_playlist}; -use crate::utils::network::request::{replace_extension, sanitize_sensitive_info}; -use crate::utils::{debug_if_enabled}; use crate::repository::playlist_repository::HLS_EXT; +use crate::utils::network::request::{replace_extension, sanitize_sensitive_info}; +use crate::utils::debug_if_enabled; +use axum::response::IntoResponse; +use bytes::Bytes; +use futures::stream; +use log::{debug, error}; +use std::sync::Arc; async fn m3u_api( api_req: &UserApiRequest, app_state: &AppState, -) -> HttpResponse { +) -> impl axum::response::IntoResponse + Send { match get_user_target(api_req, app_state).await { Some((user, target)) => { match m3u_load_rewrite_playlist(&app_state.config, target, &user).await { Ok(m3u_iter) => { // Convert the iterator into a stream of `Bytes` let content_stream = stream::iter(m3u_iter.map(|line| Ok::(Bytes::from([line.to_string().as_bytes(), b"\n"].concat())))); - let mut builder = HttpResponse::Ok(); - builder.content_type(mime::TEXT_PLAIN_UTF_8); + + let mut builder = axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::TEXT_PLAIN_UTF_8.to_string()); if api_req.content_type == "m3u_plus" { - builder.insert_header(("Content-Disposition", "attachment; filename=\"playlist.m3u\"")); + builder = builder.header("Content-Disposition", "attachment; filename=\"playlist.m3u\""); } - builder.streaming(content_stream) + builder.body(axum::body::Body::from_stream(content_stream)).unwrap().into_response() } Err(err) => { error!("{}", sanitize_sensitive_info(err.to_string().as_str())); - HttpResponse::NoContent().finish() + axum::http::StatusCode::NO_CONTENT.into_response() } } } - None => HttpResponse::BadRequest().finish(), + None => axum::http::StatusCode::BAD_REQUEST.into_response(), } } -async fn m3u_api_get(api_req: web::Query, - app_state: web::Data>, -) -> HttpResponse { - m3u_api(&api_req.into_inner(), &app_state).await -} -async fn m3u_api_post( - api_req: web::Form, - app_state: web::Data>, -) -> HttpResponse { - m3u_api(&api_req.into_inner(), &app_state).await + +#[axum::debug_handler] +async fn m3u_api_get(axum::extract::Query(api_req): axum::extract::Query, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { + m3u_api(&api_req, &app_state).await } +#[axum::debug_handler] +async fn m3u_api_post( + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Form(api_req): axum::extract::Form, +) -> impl axum::response::IntoResponse + Send { + m3u_api(&api_req, &app_state).await.into_response() +} + +#[axum::debug_handler] async fn m3u_api_stream( - req: HttpRequest, - api_req: web::Query, - path: web::Path<(String, String, String)>, - app_state: web::Data>, -) -> HttpResponse { - let (username, password, stream_id) = path.into_inner(); + req_headers: axum::http::HeaderMap, + axum::extract::Query(api_req): axum::extract::Query, + axum::extract::Path((username, password, stream_id)): axum::extract::Path<(String, String, String)>, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { let (action_stream_id, stream_ext) = separate_number_and_remainder(&stream_id); let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse()); let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state).await - else { return HttpResponse::BadRequest().finish() }; - if !user.has_permissions(&app_state) { - return HttpResponse::Forbidden().finish(); + else { return axum::http::StatusCode::BAD_REQUEST.into_response() }; + if !user.has_permissions(&app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } if !target.has_output(&TargetType::M3u) { - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } let m3u_item = match m3u_get_item_for_stream_id(virtual_id, &app_state.config, target).await { Ok(item) => item, Err(err) => { error!("Failed to get m3u url: {}", sanitize_sensitive_info(err.to_string().as_str())); - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } }; @@ -91,77 +94,82 @@ async fn m3u_api_stream( let redirect_url = if is_hls_request { &replace_extension(&m3u_item.url, "m3u8") } else { &m3u_item.url }; // TODO alias processing debug_if_enabled!("Redirecting m3u stream request to {}", sanitize_sensitive_info(redirect_url)); - return HttpResponse::Found().insert_header(("Location", redirect_url.as_str())).finish(); + return redirect(redirect_url.as_str()).into_response(); } // Reverse proxy mode if is_hls_request { let target_name = &target.name; let input = try_option_bad_request!(app_state.config.get_input_by_name(m3u_item.input_name.as_str()), true, format!("Cant find input for target {target_name}, context {}, stream_id {virtual_id}", XtreamCluster::Live)); - return handle_hls_stream_request(&app_state, &user, &m3u_item, input, TargetType::M3u).await; + return handle_hls_stream_request(&app_state, &user, &m3u_item, input, TargetType::M3u).await.into_response(); } - stream_response(&app_state, m3u_item.url.as_str(), &req, None, m3u_item.item_type, target, &user).await + stream_response(&app_state, m3u_item.url.as_str(), &req_headers, None, m3u_item.item_type, target, &user).await.into_response() } async fn m3u_api_resource( - req: HttpRequest, - api_req: web::Query, - path: web::Path<(String, String, String, String)>, - app_state: web::Data>, -) -> HttpResponse { - let (username, password, stream_id, resource) = path.into_inner(); - let Ok(m3u_stream_id) = stream_id.parse::() else { return HttpResponse::BadRequest().finish() }; + req_headers: axum::http::HeaderMap, + axum::extract::Query(api_req): axum::extract::Query, + axum::extract::Path((username, password, stream_id, resource)): axum::extract::Path<(String, String, String, String)>, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { + let Ok(m3u_stream_id) = stream_id.parse::() else { return axum::http::StatusCode::BAD_REQUEST.into_response() }; let Some((user, target)) = get_user_target_by_credentials(&username, &password, &api_req, &app_state).await - else { return HttpResponse::BadRequest().finish() }; - if !user.has_permissions(&app_state) { - return HttpResponse::Forbidden().finish(); + else { return axum::http::StatusCode::BAD_REQUEST.into_response() }; + if !user.has_permissions(&app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } if !target.has_output(&TargetType::M3u) { - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } let m3u_item = match m3u_get_item_for_stream_id(m3u_stream_id, &app_state.config, target).await { Ok(item) => item, Err(err) => { error!("Failed to get m3u url: {}", sanitize_sensitive_info(err.to_string().as_str())); - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } }; let stream_url = m3u_item.get_field(resource.as_str()); match stream_url { - None => HttpResponse::NotFound().finish(), + None => axum::http::StatusCode::NOT_FOUND.into_response(), Some(url) => { if user.proxy == ProxyType::Redirect { debug!("Redirecting stream request to {}", sanitize_sensitive_info(&url)); - HttpResponse::Found().insert_header(("Location", url.as_str())).finish() + redirect(url.as_str()).into_response() } else { - resource_response(&app_state, url.as_str(), &req, None).await + resource_response(&app_state, url.as_str(), &req_headers, None).await.into_response() } } } } macro_rules! register_m3u_stream_routes { - ($cfg:expr, [$($path:expr),*]) => {{ + ($router:expr, [$($path:expr),*]) => {{ + $router $( - $cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to(m3u_api_stream))); + .route(&format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path), axum::routing::get(m3u_api_stream)) + // $cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to(m3u_api_stream))); )* }}; } macro_rules! register_m3u_api_routes { - ($cfg:expr, [$($path:expr),*]) => {{ + ($router:expr, [$($path:expr),*]) => {{ + $router $( - $cfg.service(web::resource(format!("/{}", $path)).route(web::get().to(m3u_api_get)).route(web::post().to(m3u_api_post))); + .route(&format!("/{}", $path), axum::routing::get(m3u_api_get)) + .route(&format!("/{}", $path), axum::routing::post(m3u_api_post)) + // $cfg.service(web::resource(format!("/{}", $path)).route(web::get().to(m3u_api_get)).route(web::post().to(m3u_api_post))); )* }}; } -pub fn m3u_api_register(cfg: &mut web::ServiceConfig) { - register_m3u_api_routes!(cfg, ["get.php", "apiget", "m3u"]); - register_m3u_stream_routes!(cfg, ["live", "movie", "series"]); - cfg.service(web::resource(format!("/{M3U_STREAM_PATH}/{{username}}/{{password}}/{{stream_id}}")).route(web::get().to(m3u_api_stream))); - cfg.service(web::resource(format!("/{M3U_RESOURCE_PATH}/{{username}}/{{password}}/{{stream_id}}/{{resource}}")).route(web::get().to(m3u_api_resource))); +pub fn m3u_api_register() -> axum::Router> { + let mut router = axum::Router::new(); + router = register_m3u_api_routes!(router, ["get.php", "apiget", "m3u"]); + register_m3u_stream_routes!(router, ["live", "movie", "series"]) + .route(&format!("/{M3U_STREAM_PATH}/{{username}}/{{password}}/{{stream_id}}"), axum::routing::get(m3u_api_stream)) + .route(&format!("/{M3U_RESOURCE_PATH}/{{username}}/{{password}}/{{stream_id}}/{{resource}}"), axum::routing::get(m3u_api_resource)) } \ No newline at end of file diff --git a/src/api/endpoints/user_api.rs b/src/api/endpoints/user_api.rs index 3c1444b69..4b6adec71 100644 --- a/src/api/endpoints/user_api.rs +++ b/src/api/endpoints/user_api.rs @@ -6,18 +6,14 @@ use crate::model::playlist::XtreamCluster; use crate::model::playlist_categories::PlaylistBouquetDto; use crate::model::xtream::PlaylistXtreamCategory; use crate::repository::user_repository::{load_user_bouquet_as_json, save_user_bouquet}; -use crate::repository::{m3u_repository}; -use actix_web::body::BodyStream; -use actix_web::middleware::Compress; -use actix_web::{web, HttpResponse}; -use actix_web_httpauth::extractors::bearer::BearerAuth; -use actix_web_httpauth::middleware::HttpAuthentication; +use crate::repository::xtream_repository::xtream_get_playlist_categories; +use crate::repository::m3u_repository; use bytes::Bytes; use futures::{stream, StreamExt}; use log::error; use std::collections::HashSet; use std::sync::Arc; -use crate::repository::xtream_repository::xtream_get_playlist_categories; +use axum::response::IntoResponse; fn get_categories_from_xtream(categories: Option>) -> Vec { let mut groups: Vec = Vec::new(); @@ -30,9 +26,9 @@ fn get_categories_from_xtream(categories: Option>) - } -fn get_categories_from_m3u_playlist(target: &ConfigTarget, config: &Arc) -> Vec { +async fn get_categories_from_m3u_playlist(target: &ConfigTarget, config: &Arc) -> Vec { let mut groups = Vec::new(); - if let Some((_guard, iter)) = m3u_repository::iter_raw_m3u_playlist(config, target) { + if let Some((_guard, iter)) = m3u_repository::iter_raw_m3u_playlist(config, target).await { let mut unique_groups = HashSet::new(); for (item, _has_next) in iter { if !unique_groups.contains(item.group.as_str()) { @@ -44,14 +40,15 @@ fn get_categories_from_m3u_playlist(target: &ConfigTarget, config: &Arc) groups } +#[axum::debug_handler] async fn playlist_categories( - credentials: Option, - app_state: web::Data>, -) -> HttpResponse { - if let Some(username) = get_username_from_auth_header(credentials, &app_state) { + axum_auth::AuthBearer(token): axum_auth::AuthBearer, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { + if let Some(username) = get_username_from_auth_header(&token, &app_state) { if let Some((user, target)) = get_user_target_by_username(username.as_str(), &app_state).await { - if !user.has_permissions(&app_state) { - return HttpResponse::Forbidden().finish(); + if !user.has_permissions(&app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } let config = &app_state.config; let target_name = &target.name; @@ -73,7 +70,7 @@ async fn playlist_categories( }; let m3u_stream = if target.has_output(&TargetType::M3u) { - let live_categories = get_categories_from_m3u_playlist(target, config); + let live_categories = get_categories_from_m3u_playlist(target, config).await; stream::iter(vec![ Ok::(Bytes::from(r#"{"live": "#)), Ok::(Bytes::from(serde_json::to_string(&live_categories).unwrap_or("[]".to_string()))), @@ -90,27 +87,31 @@ async fn playlist_categories( .chain(stream::once(async { Ok::(Bytes::from("}")) })); - return HttpResponse::Ok() - .content_type(mime::APPLICATION_JSON) - .body(BodyStream::new(json_stream)); + return axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header("Content-Type", mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from_stream(json_stream)) + .unwrap() + .into_response(); } } - HttpResponse::BadRequest().finish() + axum::http::StatusCode::BAD_REQUEST.into_response() } +#[axum::debug_handler] async fn save_playlist_bouquet( - credentials: Option, - app_state: web::Data>, - req: web::Json, -) -> HttpResponse { - if let Some(username) = get_username_from_auth_header(credentials, &app_state) { + axum_auth::AuthBearer(token): axum_auth::AuthBearer, + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Json(bouquet): axum::extract::Json, +) -> impl axum::response::IntoResponse + Send { + if let Some(username) = get_username_from_auth_header(&token, &app_state) { if let Some((user, target)) = get_user_target_by_username(username.as_str(), &app_state).await { - if !user.has_permissions(&app_state) { - return HttpResponse::Forbidden().finish(); + if !user.has_permissions(&app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } - match save_user_bouquet(&app_state.config, &target.name, &username, &req.0).await { + match save_user_bouquet(&app_state.config, &target.name, &username, &bouquet).await { Ok(()) => { - return HttpResponse::Ok().finish(); + return axum::http::StatusCode::OK.into_response(); } Err(err) => { error!("Saving bouquet for {username} failed: {err}"); @@ -118,33 +119,53 @@ async fn save_playlist_bouquet( } } } - HttpResponse::BadRequest().finish() + axum::http::StatusCode::BAD_REQUEST.into_response() } +#[axum::debug_handler] async fn playlist_bouquet( - credentials: Option, - app_state: web::Data>, -) -> HttpResponse { - if let Some(username) = get_username_from_auth_header(credentials, &app_state) { + axum_auth::AuthBearer(token): axum_auth::AuthBearer, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { + if let Some(username) = get_username_from_auth_header(&token, &app_state) { if let Some((user, _target)) = get_user_target_by_username(username.as_str(), &app_state).await { - if !user.has_permissions(&app_state) { - return HttpResponse::Forbidden().finish(); + if !user.has_permissions(&app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } let xtream = load_user_bouquet_as_json(&app_state.config, &username, TargetType::Xtream).await; let m3u = load_user_bouquet_as_json(&app_state.config, &username, TargetType::M3u).await; - return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body( - format!(r#"{{"xtream": {}, "m3u": {} }}"#, xtream.unwrap_or("null".to_string()), m3u.unwrap_or("null".to_string()))); + return axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header("Content-Type", mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from(format!(r#"{{"xtream": {}, "m3u": {} }}"#, xtream.unwrap_or("null".to_string()), m3u.unwrap_or("null".to_string())))) + .unwrap() + .into_response(); } } - HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{}") + axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header("Content-Type", mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from("{}")) + .unwrap() + .into_response() } +pub fn user_api_register(app_state: Arc) -> axum::Router> { + axum::Router::new() + .nest( + "/api/v1/user", + axum::Router::new() + .route("/playlist/categories", axum::routing::get(playlist_categories)) + .route("/playlist/bouquet", axum::routing::get(playlist_bouquet)) + .route("/playlist/bouquet", axum::routing::post(save_playlist_bouquet)) + .route_layer(axum::middleware::from_fn_with_state(app_state, validator_user)) + ) -pub fn user_api_register(cfg: &mut web::ServiceConfig) { - cfg.service(web::scope("/api/v1/user") - .wrap(HttpAuthentication::with_fn(validator_user)) - .wrap(Compress::default()) - .route("/playlist/categories", web::get().to(playlist_categories)) - .route("/playlist/bouquet", web::get().to(playlist_bouquet)) - .route("/playlist/bouquet", web::post().to(save_playlist_bouquet))); + + // cfg.service(web::scope("/api/v1/user") + // .wrap(HttpAuthentication::with_fn(validator_user)) + // .wrap(Compress::default()) + // .route("/playlist/categories", web::get().to(playlist_categories)) + // .route("/playlist/bouquet", web::get().to(playlist_bouquet)) + // .route("/playlist/bouquet", web::post().to(save_playlist_bouquet))); } diff --git a/src/api/endpoints/v1_api.rs b/src/api/endpoints/v1_api.rs index 7168c201a..11500972c 100644 --- a/src/api/endpoints/v1_api.rs +++ b/src/api/endpoints/v1_api.rs @@ -1,11 +1,7 @@ use std::collections::HashSet; use std::path::PathBuf; use std::sync::Arc; - -use actix_web::body::BodyStream; -use actix_web::middleware::Condition; -use actix_web::{web, HttpResponse}; -use actix_web_httpauth::middleware::HttpAuthentication; +use axum::response::IntoResponse; use bytes::Bytes; use futures::{stream, StreamExt}; use log::error; @@ -16,7 +12,7 @@ use crate::api::endpoints::{download_api}; use crate::api::model::app_state::AppState; use crate::api::model::config::{ServerConfig, ServerInputConfig, ServerSourceConfig, ServerTargetConfig}; use crate::api::model::request::{PlaylistRequest, PlaylistRequestType}; -use crate::auth::authenticator::validator_admin; +use crate::auth::authenticator::{validator_admin}; use crate::m3u_filter_error::M3uFilterError; use crate::model::api_proxy::{ApiProxyConfig, ApiProxyServerInfo, TargetUser}; use crate::model::config::{validate_targets, Config, ConfigDto, ConfigInput, ConfigInputOptions, ConfigSource, ConfigTarget, InputType, TargetType}; @@ -53,100 +49,99 @@ fn intern_save_config_main(file_path: &str, backup_dir: &str, cfg: &ConfigDto) - } async fn save_config_api_proxy_user( - req: web::Json>, - app_state: web::Data>, -) -> HttpResponse { - let mut users = req.0; + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Json(mut users): axum::extract::Json>, +) -> impl axum::response::IntoResponse + Send { let mut usernames = HashSet::new(); let mut tokens = HashSet::new(); for target_user in &mut users { for credential in &mut target_user.credentials { credential.trim(); if let Err(err) = credential.validate() { - return HttpResponse::BadRequest().json(json!({"error": err.to_string()})); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": err.to_string()}))).into_response(); } if usernames.contains(&credential.username) { - return HttpResponse::BadRequest().json(json!({"error": format!("Duplicate username {}", &credential.username)})); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate username {}", &credential.username)}))).into_response(); } usernames.insert(&credential.username); if let Some(token) = &credential.token { if tokens.contains(token) { - return HttpResponse::BadRequest().json(json!({"error": format!("Duplicate token {token}")})); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": format!("Duplicate token {token}")}))).into_response(); } tokens.insert(token); } } } - if let Some(api_proxy) = app_state.config.t_api_proxy.write().as_mut() { + let mut lock = app_state.config.t_api_proxy.write().await; + if let Some(api_proxy) = lock.as_mut() { api_proxy.user = users; api_proxy.user.iter_mut().flat_map(|t| &mut t.credentials).for_each(|c| c.prepare(true)); if api_proxy.use_user_db { if let Err(err) = store_api_user(&app_state.config, &api_proxy.user) { - return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); + return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response(); } } else { let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str(); if let Some(err) = intern_save_config_api_proxy(backup_dir, api_proxy, app_state.config.t_api_proxy_file_path.as_str()) { - return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); + return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response(); } } } - HttpResponse::Ok().finish() + axum::http::StatusCode::OK.into_response() } async fn save_config_main( - req: web::Json, - app_state: web::Data>, -) -> HttpResponse { - let cfg = req.0; + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Json(cfg): axum::extract::Json, +) -> impl axum::response::IntoResponse + Send { if cfg.is_valid() { let file_path = app_state.config.t_config_file_path.as_str(); let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str(); if let Some(err) = intern_save_config_main(file_path, backup_dir, &cfg) { - return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); + return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response(); } - HttpResponse::Ok().finish() + axum::http::StatusCode::OK.into_response() } else { - HttpResponse::BadRequest().json(json!({"error": "Invalid content"})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid content"}))).into_response() } } async fn save_config_api_proxy_config( - req: web::Json>, - app_state: web::Data>, -) -> HttpResponse { - let mut req_api_proxy = req.0; + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Json(mut req_api_proxy): axum::extract::Json>, +) -> impl axum::response::IntoResponse + Send { for server_info in &mut req_api_proxy { if !server_info.is_valid() { - return HttpResponse::BadRequest().json(json!({"error": "Invalid content"})); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid content"}))).into_response(); } } - if let Some(api_proxy) = app_state.config.t_api_proxy.write().as_mut() { + let mut lock = app_state.config.t_api_proxy.write().await; + if let Some(api_proxy) = lock.as_mut() { api_proxy.server = req_api_proxy; let backup_dir = app_state.config.backup_dir.as_ref().unwrap().as_str(); if let Some(err) = intern_save_config_api_proxy(backup_dir, api_proxy, app_state.config.t_api_proxy_file_path.as_str()) { - return HttpResponse::InternalServerError().json(json!({"error": err.to_string()})); + return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, axum::Json(json!({"error": err.to_string()}))).into_response(); } } - HttpResponse::Ok().finish() + axum::http::StatusCode::OK.into_response() } +#[axum::debug_handler] async fn playlist_update( - req: web::Json>, - app_state: web::Data>, -) -> HttpResponse { - let targets = req.0; + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Json(targets): axum::extract::Json>, +) -> impl axum::response::IntoResponse + Send { let user_targets = if targets.is_empty() { None } else { Some(targets) }; let process_targets = validate_targets(user_targets.as_ref(), &app_state.config.sources); match process_targets { Ok(valid_targets) => { - actix_rt::spawn(playlist::exec_processing(Arc::clone(&app_state.http_client), Arc::clone(&app_state.config), Arc::new(valid_targets))); - HttpResponse::Ok().finish() + tokio::spawn(playlist::exec_processing(Arc::clone(&app_state.http_client), Arc::clone(&app_state.config), Arc::new(valid_targets))); + axum::http::StatusCode::OK.into_response() } Err(err) => { error!("Failed playlist update {}", sanitize_sensitive_info(err.to_string().as_str())); - HttpResponse::BadRequest().json(json!({"error": err.to_string()})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": err.to_string()}))).into_response() } } } @@ -189,7 +184,7 @@ fn create_config_input_for_xtream(username: &str, password: &str, host: &str) -> } } -async fn get_playlist(client: Arc, cfg_input: Option<&ConfigInput>, cfg: &Config) -> HttpResponse { +async fn get_playlist(client: Arc, cfg_input: Option<&ConfigInput>, cfg: &Config) -> impl axum::response::IntoResponse + Send { match cfg_input { Some(input) => { let (result, errors) = @@ -199,12 +194,12 @@ async fn get_playlist(client: Arc, cfg_input: Option<&ConfigInp }; if result.is_empty() { let error_strings: Vec = errors.iter().map(std::string::ToString::to_string).collect(); - HttpResponse::BadRequest().json(json!({"error": error_strings.join(", ")})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": error_strings.join(", ")}))).into_response() } else { - HttpResponse::Ok().json(result) + (axum::http::StatusCode::OK, axum::Json(result)).into_response() } } - None => HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})), + None => (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response(), } } @@ -222,7 +217,7 @@ async fn get_categories_content(action: Result<(Option, Option) None } -async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc) -> HttpResponse { +async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc) -> impl axum::response::IntoResponse + Send { if let Some(target) = cfg_target { let target_name = &target.name; if target.has_output(&TargetType::Xtream) { @@ -230,9 +225,9 @@ async fn get_playlist_for_target(cfg_target: Option<&ConfigTarget>, cfg: &Arc, cfg: &Arc(Bytes::from(r"]}}")), ])); - return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(BodyStream::new(json_stream)); + return (axum::http::StatusCode::OK, axum::body::Body::from_stream(json_stream)).into_response(); } else if target.has_output(&TargetType::M3u) { - return HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})); + return (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response(); } } - HttpResponse::BadRequest().json(json!({"error": "Invalid Arguments"})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid Arguments"}))).into_response() } -async fn playlist( - req: web::Json, - app_state: web::Data>, -) -> HttpResponse { - match req.rtype { +#[axum::debug_handler] +async fn playlist_content( + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Json(playlist_req): axum::extract::Json, +) -> impl axum::response::IntoResponse + Send { + match playlist_req.rtype { PlaylistRequestType::Input => { - if let Some(source_id) = req.source_id { - get_playlist(Arc::clone(&app_state.http_client), app_state.config.get_input_by_id(source_id), &app_state.config).await + if let Some(source_id) = playlist_req.source_id { + get_playlist(Arc::clone(&app_state.http_client), app_state.config.get_input_by_id(source_id), &app_state.config).await.into_response() } else { - HttpResponse::BadRequest().json(json!({"error": "Invalid input"})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid input"}))).into_response() } } PlaylistRequestType::Target => { - if let Some(source_id) = req.source_id { - get_playlist_for_target(app_state.config.get_target_by_id(source_id), &app_state.config).await + if let Some(source_id) = playlist_req.source_id { + get_playlist_for_target(app_state.config.get_target_by_id(source_id), &app_state.config).await.into_response() } else { - HttpResponse::BadRequest().json(json!({"error": "Invalid target"})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid target"}))).into_response() } } PlaylistRequestType::Xtream => { - if let (Some(url), Some(username), Some(password)) = (req.url.as_ref(), req.username.as_ref(), req.password.as_ref()) { + if let (Some(url), Some(username), Some(password)) = (playlist_req.url.as_ref(), playlist_req.username.as_ref(), playlist_req.password.as_ref()) { let input = create_config_input_for_xtream(username, password, url); - get_playlist(Arc::clone(&app_state.http_client), Some(&input), &app_state.config).await + get_playlist(Arc::clone(&app_state.http_client), Some(&input), &app_state.config).await.into_response() } else { - HttpResponse::BadRequest().json(json!({"error": "Invalid url"})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid url"}))).into_response() } } PlaylistRequestType::M3U => { - if let Some(url) = req.url.as_ref() { + if let Some(url) = playlist_req.url.as_ref() { let input = create_config_input_for_m3u(url); - get_playlist(Arc::clone(&app_state.http_client), Some(&input), &app_state.config).await + get_playlist(Arc::clone(&app_state.http_client), Some(&input), &app_state.config).await.into_response() } else { - HttpResponse::BadRequest().json(json!({"error": "Invalid url"})) + (axum::http::StatusCode::BAD_REQUEST, axum::Json(json!({"error": "Invalid url"}))).into_response() } } } } async fn config( - app_state: web::Data>, -) -> HttpResponse { + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { let map_input = |i: &ConfigInput| ServerInputConfig { id: i.id, name: i.name.clone(), @@ -363,24 +359,27 @@ async fn config( // if we didn't read it from file then we should use it from app_state if result.api_proxy.is_none() { - result.api_proxy.clone_from(&*app_state.config.t_api_proxy.read()); + result.api_proxy.clone_from(&*app_state.config.t_api_proxy.read().await); } - HttpResponse::Ok().json(result) + axum::response::Json(result).into_response() } -pub fn v1_api_register(web_auth_enabled: bool) -> impl Fn(&mut web::ServiceConfig) { - move |cfg: &mut web::ServiceConfig| { - user_api_register(cfg); - cfg.service(web::scope("/api/v1") - .wrap(Condition::new(web_auth_enabled, HttpAuthentication::with_fn(validator_admin))) - .route("/config", web::get().to(config)) - .route("/config/main", web::post().to(save_config_main)) - .route("/config/user", web::post().to(save_config_api_proxy_user)) - .route("/config/apiproxy", web::post().to(save_config_api_proxy_config)) - .route("/playlist", web::post().to(playlist)) - .route("/playlist/update", web::post().to(playlist_update)) - .route("/file/download", web::post().to(download_api::queue_download_file)) - .route("/file/download/info", web::get().to(download_api::download_file_info))); +pub fn v1_api_register(web_auth_enabled: bool, app_state: Arc) -> axum::Router> { + let mut router = axum::Router::new(); + router = router.route("/config", axum::routing::get(config)) + .route("/config/main", axum::routing::post(save_config_main)) + .route("/config/user", axum::routing::post(save_config_api_proxy_user)) + .route("/config/apiproxy", axum::routing::post(save_config_api_proxy_config)) + .route("/playlist", axum::routing::post(playlist_content)) + .route("/playlist/update", axum::routing::post(playlist_update)) + .route("/file/download", axum::routing::post(download_api::queue_download_file)) + .route("/file/download/info", axum::routing::get(download_api::download_file_info)); + if web_auth_enabled { + router = router.route_layer(axum::middleware::from_fn_with_state(Arc::clone(&app_state), validator_admin)); } + + axum::Router::new() + .merge(user_api_register(app_state)) + .nest("/api/v1", router) } diff --git a/src/api/endpoints/web_index.rs b/src/api/endpoints/web_index.rs index af5a5335d..f7a86ad2d 100644 --- a/src/api/endpoints/web_index.rs +++ b/src/api/endpoints/web_index.rs @@ -1,27 +1,26 @@ use std::sync::Arc; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use actix_files::NamedFile; -use actix_web::{HttpRequest, HttpResponse, web}; -use actix_web_httpauth::extractors::bearer::BearerAuth; +use axum::response::IntoResponse; +use crate::api::api_utils::serve_file; use crate::api::model::app_state::AppState; use crate::auth::authenticator::{create_jwt_admin, create_jwt_user, is_admin, verify_token}; use crate::auth::password::verify_password; use crate::auth::user::UserCredential; -fn no_web_auth_token() -> HttpResponse { - HttpResponse::Ok().json(HashMap::from([("token", "authorized")])) +fn no_web_auth_token() -> impl axum::response::IntoResponse + Send { + axum::Json(HashMap::from([("token", "authorized")])).into_response() } async fn token( - mut req: web::Json, - app_state: web::Data>, -) -> HttpResponse { + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Json(mut req): axum::extract::Json, +) -> impl axum::response::IntoResponse + Send { match &app_state.config.web_auth { - None => no_web_auth_token(), + None => no_web_auth_token().into_response(), Some(web_auth) => { if !web_auth.enabled { - return no_web_auth_token(); + return no_web_auth_token().into_response(); } let username = req.username.as_str(); let password = req.password.as_str(); @@ -31,41 +30,38 @@ async fn token( if verify_password(hash, password.as_bytes()) { if let Ok(token) = create_jwt_admin(web_auth, username) { req.zeroize(); - return HttpResponse::Ok().json(HashMap::from([("token", token)])); + return axum::Json(HashMap::from([("token", token)])).into_response(); } } } - if let Some(credentials) = app_state.config.get_user_credentials(username) { + if let Some(credentials) = app_state.config.get_user_credentials(username).await { if credentials.password == password { if let Ok(token) = create_jwt_user(web_auth, username) { req.zeroize(); - return HttpResponse::Ok().json(HashMap::from([("token", token)])); + return axum::Json(HashMap::from([("token", token)])).into_response(); } } } } req.zeroize(); - HttpResponse::BadRequest().finish() + axum::http::StatusCode::BAD_REQUEST.into_response() } } } async fn token_refresh( - _req: HttpRequest, - credentials: Option, - app_state: web::Data>, -) -> HttpResponse { + axum_auth::AuthBearer(token): axum_auth::AuthBearer, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { match &app_state.config.web_auth { - None => { - no_web_auth_token() - }, + None => no_web_auth_token().into_response(), Some(web_auth) => { if !web_auth.enabled { - return no_web_auth_token(); + return no_web_auth_token().into_response(); } let secret_key = web_auth.secret.as_ref(); - let maybe_token_data = verify_token(credentials, secret_key); + let maybe_token_data = verify_token(&token, secret_key); if let Some(token_data) = maybe_token_data { let username = token_data.claims.username.clone(); let web_auth_cfg = app_state.config.web_auth.as_ref().unwrap(); @@ -75,29 +71,37 @@ async fn token_refresh( create_jwt_user(web_auth_cfg, &username) }; if let Ok(token) = new_token { - return HttpResponse::Ok().json(HashMap::from([("token", token)])); + return axum::Json(HashMap::from([("token", token)])).into_response(); } } - HttpResponse::BadRequest().finish() + axum::http::StatusCode::BAD_REQUEST.into_response() } } } async fn index( - _req: HttpRequest, - app_state: web::Data>, -) -> std::io::Result { + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { let path: PathBuf = [&app_state.config.api.web_root, "index.html"].iter().collect(); - NamedFile::open(path) + serve_file(&path, mime::TEXT_HTML_UTF_8).await.into_response() } -pub fn index_register(web_dir_path: &Path) -> impl Fn(&mut web::ServiceConfig) + '_ { - move |cfg: &mut web::ServiceConfig| { - cfg.service(web::scope("/auth") - .route("/token", web::post().to(token)) - .route("/refresh", web::post().to(token_refresh))); - cfg.service(web::scope("") - .route("/", web::get().to(index)) - .service(actix_files::Files::new("", web_dir_path))); - } -} \ No newline at end of file +pub fn index_register(web_dir_path: &Path) -> axum::Router> { + axum::Router::new() + .nest("/auth", axum::Router::new() + .route("/token", axum::routing::post(token)) + .route("/refresh", axum::routing::post(token_refresh))) + .merge(axum::Router::new() + .route("/", axum::routing::get(index)) + .fallback(axum::routing::get_service(tower_http::services::ServeDir::new(web_dir_path)))) +} +// pub fn index_register(web_dir_path: &Path) -> impl Fn(&mut web::ServiceConfig) + '_ { +// move |cfg: &mut web::ServiceConfig| { +// cfg.service(web::scope("/auth") +// .route("/token", web::post().to(token)) +// .route("/refresh", web::post().to(token_refresh))); +// cfg.service(web::scope("") +// .route("/", web::get().to(index)) +// .service(actix_files::Files::new("", web_dir_path))); +// } +// } \ No newline at end of file diff --git a/src/api/endpoints/xmltv_api.rs b/src/api/endpoints/xmltv_api.rs index d170436fc..078069c0a 100644 --- a/src/api/endpoints/xmltv_api.rs +++ b/src/api/endpoints/xmltv_api.rs @@ -1,14 +1,14 @@ -use std::sync::Arc; use std::fs::File; use std::path::{Path, PathBuf}; - -use actix_web::{http::header, web, HttpRequest, HttpResponse}; -use log::{error, trace}; -use quick_xml::{Reader, Writer}; +use std::sync::Arc; +use axum::response::IntoResponse; +use chrono::{Duration, NaiveDateTime, TimeDelta}; use flate2::write::GzEncoder; use flate2::Compression; +// use actix_web::{http::header, web, HttpRequest, HttpResponse}; +use log::{error, trace}; use quick_xml::events::{BytesStart, Event}; -use chrono::{Duration, NaiveDateTime, TimeDelta}; +use quick_xml::{Reader, Writer}; use crate::api::api_utils::{get_user_target, serve_file}; use crate::api::model::app_state::AppState; @@ -21,9 +21,12 @@ use crate::repository::xtream_repository::{xtream_get_epg_file_path, xtream_get_ use crate::utils::file::file_utils; use crate::utils::file::file_utils::file_reader; -pub fn get_empty_epg_response() -> HttpResponse { - HttpResponse::Ok().content_type(mime::TEXT_XML).body( - r#""#) +pub fn get_empty_epg_response() -> impl axum::response::IntoResponse + Send { + axum::response::Response::builder() + .status(axum::http::StatusCode::OK) // Entspricht `HttpResponse::Ok()` + .header(axum::http::header::CONTENT_TYPE, axum::http::HeaderValue::from_static("text/xml")) + .body(axum::body::Body::from(r#""#)) // Setzt den Body der Antwort + .unwrap() } fn time_correct(date_time: &str, correction: &TimeDelta) -> String { @@ -35,12 +38,12 @@ fn time_correct(date_time: &str, correction: &TimeDelta) -> String { // Parse the datetime string NaiveDateTime::parse_from_str(date_time_split[0], "%Y%m%d%H%M%S").map_or_else(|_| date_time.to_string(), |native_dt| { - let corrected_dt = native_dt + *correction; - // Format the corrected datetime back to string - let formatted_dt = corrected_dt.format("%Y%m%d%H%M%S").to_string(); - let result = format!("{} {}", formatted_dt, date_time_split[1]); - result - }) + let corrected_dt = native_dt + *correction; + // Format the corrected datetime back to string + let formatted_dt = corrected_dt.format("%Y%m%d%H%M%S").to_string(); + let result = format!("{} {}", formatted_dt, date_time_split[1]); + result + }) } fn get_epg_path_for_target_of_type(target_name: &str, epg_path: PathBuf) -> Option { @@ -89,23 +92,23 @@ fn parse_timeshift(time_shift: Option<&String>) -> Option { }) } -async fn serve_epg(epg_path: &Path, req: &HttpRequest, user: &ProxyUserCredentials) -> HttpResponse { +async fn serve_epg(epg_path: &Path, user: &ProxyUserCredentials) -> impl axum::response::IntoResponse + Send { match File::open(epg_path) { Ok(epg_file) => { match parse_timeshift(user.epg_timeshift.as_ref()) { - None => serve_file(epg_path, req, mime::TEXT_XML).await, + None => serve_file(epg_path, mime::TEXT_XML).await.into_response(), Some(duration) => { - serve_epg_with_timeshift(epg_file, duration) + serve_epg_with_timeshift(epg_file, duration).into_response() } } } Err(_) => { - get_empty_epg_response() + get_empty_epg_response().into_response() } } } -fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> HttpResponse { +fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> impl axum::response::IntoResponse + Send { let reader = file_reader(epg_file); let encoder = GzEncoder::new(Vec::with_capacity(4096), Compression::default()); let mut xml_reader = Reader::from_reader(reader); @@ -160,36 +163,38 @@ fn serve_epg_with_timeshift(epg_file: File, offset_minutes: i32) -> HttpResponse } let compressed_data = xml_writer.into_inner().finish().unwrap(); - HttpResponse::Ok() - .content_type("application/octet-stream") - .insert_header((header::CONTENT_ENCODING, "gzip")) // Set Content-Encoding header - .body(compressed_data) + axum::response::Response::builder() + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM.to_string()) + .header(axum::http::header::CONTENT_ENCODING, "gzip") // Set Content-Encoding header + .body(axum::body::Body::from(compressed_data)) + .unwrap() + .into_response() } async fn xmltv_api( - api_req: web::Query, - req: HttpRequest, - app_state: web::Data>, -) -> HttpResponse { + axum::extract::Query(api_req): axum::extract::Query, + axum::extract::State(app_state): axum::extract::State>, +) -> impl axum::response::IntoResponse + Send { if let Some((user, target)) = get_user_target(&api_req, &app_state).await { - if !user.has_permissions(&app_state) { - return HttpResponse::Forbidden().finish(); + if !user.has_permissions(&app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } match get_epg_path_for_target(&app_state.config, target) { None => { // No epg configured, No processing or timeshift, epg can't be mapped to the channels. // we do not deliver epg } - Some(epg_path) => return serve_epg(&epg_path, &req, &user).await + Some(epg_path) => return serve_epg(&epg_path, &user).await.into_response() } } - get_empty_epg_response() + get_empty_epg_response().into_response() } -pub fn xmltv_api_register(cfg: &mut web::ServiceConfig) { - cfg.service(web::resource("/xmltv.php").route(web::get().to(xmltv_api))) - .service(web::resource("/update/epg.php").route(web::get().to(xmltv_api))) - .service(web::resource("/epg").route(web::get().to(xmltv_api))); +pub fn xmltv_api_register() -> axum::Router> { + axum::Router::new() + .route("/xmltv.php", axum::routing::get(xmltv_api)) + .route("/update/epg.php", axum::routing::get(xmltv_api)) + .route("/epg", axum::routing::get(xmltv_api)) } #[cfg(test)] diff --git a/src/api/endpoints/xtream_api.rs b/src/api/endpoints/xtream_api.rs index 27e19d298..b7dce6b55 100644 --- a/src/api/endpoints/xtream_api.rs +++ b/src/api/endpoints/xtream_api.rs @@ -1,18 +1,19 @@ // https://github.com/tellytv/go.xtream-codes/blob/master/structs.go -use crate::api::api_utils::{try_option_bad_request, try_result_bad_request}; +use crate::api::api_utils::{redirect, try_option_bad_request, try_result_bad_request}; use crate::utils::trace_if_enabled; use std::collections::{HashMap, HashSet}; use std::fmt::{Display, Formatter}; use std::path::Path; -use std::rc::Rc; use std::str::FromStr; use std::sync::Arc; -use actix_web::{web, HttpRequest, HttpResponse}; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::IntoResponse; use bytes::Bytes; use futures::stream::{self, StreamExt}; use futures::Stream; use log::{debug, error, warn}; +use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use crate::api::api_utils; use crate::api::api_utils::{get_user_target, get_user_target_by_credentials, resource_response, separate_number_and_remainder, serve_file, stream_response}; @@ -48,7 +49,7 @@ const TAG_CATEGORY_ID: &str = "category_id"; const TAG_STREAM_ID: &str = "stream_id"; const TAG_EPG_LISTINGS: &str = "epg_listings"; -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] enum XtreamApiStreamContext { LiveAlt, Live, @@ -75,7 +76,7 @@ impl Display for XtreamApiStreamContext { } } -#[derive(Debug)] +#[derive(Serialize, Deserialize, Debug)] struct XtreamApiStreamRequest<'a> { context: XtreamApiStreamContext, username: &'a str, @@ -100,9 +101,9 @@ impl<'a> XtreamApiStreamRequest<'a> { } } -pub fn serve_query(file_path: &Path, filter: &HashMap<&str, HashSet>) -> HttpResponse { +pub fn serve_query(file_path: &Path, filter: &HashMap<&str, HashSet>) -> impl axum::response::IntoResponse + Send { let filtered = json_utils::json_filter_file(file_path, filter); - HttpResponse::Ok().json(filtered) + axum::Json(filtered) } fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &XtreamApiStreamContext, action_path: &str, fallback_url: &str) -> Option { @@ -132,26 +133,26 @@ fn get_xtream_player_api_stream_url(input: &ConfigInput, context: &XtreamApiStre } } -fn get_user_info(user: &ProxyUserCredentials, app_state: &AppState) -> XtreamAuthorizationResponse { - let server_info = app_state.config.get_user_server_info(user); - let active_connections = app_state.get_active_connections_for_user(&user.username); +async fn get_user_info(user: &ProxyUserCredentials, app_state: &AppState) -> XtreamAuthorizationResponse { + let server_info = app_state.config.get_user_server_info(user).await; + let active_connections = app_state.get_active_connections_for_user(&user.username).await; XtreamAuthorizationResponse::new(&server_info, user, active_connections, app_state.config.user_access_control) } async fn xtream_player_api_stream( - req: &HttpRequest, - api_req: &web::Query, - app_state: &web::Data>, + req_headers: &HeaderMap, + api_req: &UserApiRequest, + app_state: &Arc, stream_req: XtreamApiStreamRequest<'_>, -) -> HttpResponse { +) -> impl axum::response::IntoResponse + Send { let (user, target) = try_option_bad_request!(get_user_target_by_credentials(stream_req.username, stream_req.password, api_req, app_state).await, false, format!("Could not find any user {}", stream_req.username)); - if !user.has_permissions(app_state) { - return HttpResponse::Forbidden().finish(); + if !user.has_permissions(app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } let target_name = &target.name; if !target.has_output(&TargetType::Xtream) { debug!("Target has no xtream output {}", target_name); - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } let (action_stream_id, stream_ext) = separate_number_and_remainder(stream_req.stream_id); let virtual_id: u32 = try_result_bad_request!(action_stream_id.trim().parse()); @@ -160,7 +161,7 @@ async fn xtream_player_api_stream( if pli.item_type == PlaylistItemType::LiveHls { debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(&pli.url)); - return HttpResponse::Found().insert_header(("Location", pli.url.to_string())).finish(); + return redirect(&pli.url).into_response(); } let is_hls_request = stream_ext.as_deref() == Some(HLS_EXT); @@ -172,17 +173,17 @@ async fn xtream_player_api_stream( let username = input.username.as_ref().map_or("", |v| v); let password = input.password.as_ref().map_or("", |v| v); let stream_url = format!("{url}/series/{username}/{password}/{}{ext}", mapping.provider_id); - return HttpResponse::Found().insert_header(("Location", stream_url)).finish(); + return redirect(&stream_url).into_response(); } let redirect_url = if is_hls_request { &replace_extension(&pli.url, "m3u8") } else { &pli.url }; debug_if_enabled!("Redirecting stream request to {}", sanitize_sensitive_info(redirect_url)); - return HttpResponse::Found().insert_header(("Location", redirect_url.as_str())).finish(); + return redirect(redirect_url.as_str()).into_response(); } // Reverse proxy mode if is_hls_request { - return handle_hls_stream_request(app_state, &user, &pli, input, TargetType::Xtream).await; + return handle_hls_stream_request(app_state, &user, &pli, input, TargetType::Xtream).await.into_response(); } let extension = stream_ext.unwrap_or_else( @@ -200,7 +201,7 @@ async fn xtream_player_api_stream( stream_req.context)); trace_if_enabled!("Streaming stream request from {}", sanitize_sensitive_info(&stream_url)); - stream_response(app_state, &stream_url, req, Some(input), pli.item_type, target, &user).await + stream_response(app_state, &stream_url, req_headers, Some(input), pli.item_type, target, &user).await.into_response() } @@ -215,18 +216,18 @@ fn get_doc_id_and_field_name(input: &str) -> Option<(u32, &str)> { None } -fn get_doc_resource_field_value(field: &str, doc: Option<&Value>) -> Option> { +fn get_doc_resource_field_value(field: &str, doc: Option<&Value>) -> Option { if let Some(Value::Object(info_data)) = doc { if field.starts_with(PROP_BACKDROP_PATH) { return get_backdrop_path_value(field, info_data.get(PROP_BACKDROP_PATH)); } else if let Some(Value::String(url)) = info_data.get(field) { - return Some(Rc::new(url.to_string())); + return Some(url.to_string()); } } None } -fn xtream_get_info_resource_url(config: &Config, pli: &XtreamPlaylistItem, target: &ConfigTarget, resource: &str) -> Result>, serde_json::Error> { +fn xtream_get_info_resource_url(config: &Config, pli: &XtreamPlaylistItem, target: &ConfigTarget, resource: &str) -> Result, serde_json::Error> { let info_content = match pli.xtream_cluster { XtreamCluster::Video => { xtream_repository::xtream_load_vod_info(config, target.name.as_str(), pli.get_virtual_id()) @@ -301,7 +302,7 @@ fn get_season_info_doc(doc: &Vec, season_id: u32) -> Option<&Value> { } -fn xtream_get_season_resource_url(config: &Config, pli: &XtreamPlaylistItem, target: &ConfigTarget, resource: &str) -> Result>, serde_json::Error> { +fn xtream_get_season_resource_url(config: &Config, pli: &XtreamPlaylistItem, target: &ConfigTarget, resource: &str) -> Result, serde_json::Error> { let info_content = match pli.xtream_cluster { XtreamCluster::Series => { xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.get_virtual_id()) @@ -330,19 +331,19 @@ fn xtream_get_season_resource_url(config: &Config, pli: &XtreamPlaylistItem, tar } async fn xtream_player_api_resource( - req: &HttpRequest, - api_req: &web::Query, - app_state: &web::Data>, + req_headers: &HeaderMap, + api_req: &UserApiRequest, + app_state: &Arc, resource_req: XtreamApiStreamRequest<'_>, -) -> HttpResponse { +) -> impl axum::response::IntoResponse { let (user, target) = try_option_bad_request!(get_user_target_by_credentials(resource_req.username, resource_req.password, api_req, app_state).await, false, format!("Could not find any user {}", resource_req.username)); - if !user.has_permissions(app_state) { - return HttpResponse::Forbidden().finish(); + if !user.has_permissions(app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } let target_name = &target.name; if !target.has_output(&TargetType::Xtream) { debug!("Target has no xtream output {}", target_name); - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } let virtual_id: u32 = try_result_bad_request!(resource_req.stream_id.trim().parse()); let resource = resource_req.action_path.trim(); @@ -356,43 +357,41 @@ async fn xtream_player_api_resource( }; match stream_url { - None => HttpResponse::NotFound().finish(), + None => axum::http::StatusCode::NOT_FOUND.into_response(), Some(url) => { if user.proxy == ProxyType::Redirect { trace_if_enabled!("Redirecting resource request to {}", sanitize_sensitive_info(&url)); - HttpResponse::Found().insert_header(("Location", url.as_str())).finish() + redirect(url.as_str()).into_response() } else { trace_if_enabled!("Resource request to {}", sanitize_sensitive_info(&url)); - resource_response(app_state, url.as_str(), req, None).await + resource_response(app_state, url.as_str(), req_headers, None).await.into_response() } } } } -macro_rules! create_xtream_player_api_stream { - ($fn_name:ident, $context:expr) => { - async fn $fn_name( - req: HttpRequest, - api_req: web::Query, - path: web::Path<(String, String, String)>, - app_state: web::Data>, - ) -> HttpResponse { - let (username, password, stream_id) = path.into_inner(); - xtream_player_api_stream(&req, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, "")).await - } - } -} - macro_rules! create_xtream_player_api_resource { ($fn_name:ident, $context:expr) => { async fn $fn_name( - req: HttpRequest, - api_req: web::Query, - path: web::Path<(String, String, String, String)>, - app_state: web::Data>, - ) -> HttpResponse { - let (username, password, stream_id, resource) = path.into_inner(); - xtream_player_api_resource(&req, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, &resource)).await + axum::extract::Path((username, password, stream_id, resource)): axum::extract::Path<(String, String, String, String)>, + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Query(api_req): axum::extract::Query, + req_headers: HeaderMap, + ) -> impl axum::response::IntoResponse { + xtream_player_api_resource(&req_headers, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, &resource)).await.into_response() + } + } +} + +macro_rules! create_xtream_player_api_stream { + ($fn_name:ident, $context:expr) => { + async fn $fn_name( + axum::extract::Path((username, password, stream_id)): axum::extract::Path<(String, String, String)>, + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Query(api_req): axum::extract::Query, + req_headers: HeaderMap, + ) -> impl axum::response::IntoResponse + Send { + xtream_player_api_stream(&req_headers, &api_req, &app_state, XtreamApiStreamRequest::from($context, &username, &password, &stream_id, "")).await.into_response() } } } @@ -416,41 +415,46 @@ fn get_non_empty<'a>(first: &'a str, second: &'a str, third: &'a str) -> &'a str } } +#[axum::debug_handler] async fn xtream_player_api_timeshift_stream( - req: HttpRequest, - api_query_req: web::Query, - api_form_req: web::Form, - path: web::Path<(String, String, String, String, String)>, - app_state: web::Data>, -) -> HttpResponse { - let (path_username, path_password, path_duration, path_start, path_stream_id) = path.into_inner(); + req_headers: HeaderMap, + axum::extract::Query(api_query_req): axum::extract::Query, + axum::extract::Path((path_username, path_password, path_duration, path_start, path_stream_id)): axum::extract::Path<(String, String, String, String, String)>, + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Form(api_form_req): axum::extract::Form, +) -> impl axum::response::IntoResponse + Send { let username = get_non_empty(&path_username, &api_query_req.username, &api_form_req.username); let password = get_non_empty(&path_password, &api_query_req.password, &api_form_req.password); let stream_id = get_non_empty(&path_stream_id, &api_query_req.stream, &api_form_req.stream); let duration = get_non_empty(&path_duration, &api_query_req.duration, &api_form_req.duration); let start = get_non_empty(&path_start, &api_query_req.start, &api_form_req.start); let action_path = format!("{duration}/{start}"); - xtream_player_api_stream(&req, &api_query_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Timeshift, username, password, stream_id, &action_path)).await + xtream_player_api_stream(&req_headers, &api_query_req, &app_state, XtreamApiStreamRequest::from(XtreamApiStreamContext::Timeshift, username, password, stream_id, &action_path)).await } async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, - cluster: XtreamCluster) -> HttpResponse { + cluster: XtreamCluster) -> impl axum::response::IntoResponse + Send { let virtual_id: u32 = match FromStr::from_str(stream_id) { Ok(id) => id, - Err(_) => return HttpResponse::BadRequest().finish() + Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response() }; if let Ok((pli, virtual_record)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(cluster)) { if pli.provider_id > 0 { - let input_name = Rc::clone(&pli.input_name); + let input_name = &pli.input_name; if let Some(input) = app_state.config.get_input_by_name(input_name.as_str()) { if let Some(info_url) = xtream::get_xtream_player_api_info_url(input, cluster, pli.provider_id) { // Redirect is only possible for live streams, vod and series info needs to be modified if user.proxy == ProxyType::Redirect && cluster == XtreamCluster::Live { - return HttpResponse::Found().insert_header(("Location", info_url)).finish(); + return redirect(&info_url).into_response(); } else if let Ok(content) = xtream::get_xtream_stream_info(Arc::clone(&app_state.http_client), &app_state.config, user, input, target, &pli, info_url.as_str(), cluster).await { - return HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content); + return axum::response::Response::builder() + .status(StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from(content)) + .unwrap() + .into_response() } } } @@ -459,25 +463,40 @@ async fn xtream_get_stream_info_response(app_state: &AppState, user: &ProxyUserC return match cluster { XtreamCluster::Video => { let content = create_vod_info_from_item(user, &pli, virtual_record.last_updated); - HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content) + axum::response::Response::builder() + .status(StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from(content)) + .unwrap() + .into_response() } - XtreamCluster::Live | XtreamCluster::Series => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{}"), + XtreamCluster::Live | XtreamCluster::Series => axum::response::Response::builder() + .status(StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from("{}".as_bytes())) + .unwrap() + .into_response(), }; } - HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body("{}") + axum::response::Response::builder() + .status(StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from("{}".as_bytes())) + .unwrap() + .into_response() } -async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, limit: &str) -> HttpResponse { +async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, target: &ConfigTarget, stream_id: &str, limit: &str) -> impl axum::response::IntoResponse + Send { let target_name = &target.name; if target.has_output(&TargetType::Xtream) { let virtual_id: u32 = match FromStr::from_str(stream_id.trim()) { Ok(id) => id, - Err(_) => return HttpResponse::BadRequest().finish() + Err(_) => return axum::http::StatusCode::BAD_REQUEST.into_response() }; if let Ok((pli, _)) = xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, None) { if pli.provider_id > 0 { - let input_name = Rc::clone(&pli.input_name); + let input_name = &pli.input_name; if let Some(input) = app_state.config.get_input_by_name(input_name.as_str()) { if let Some(action_url) = xtream::get_xtream_player_api_action_url(input, ACTION_GET_SHORT_EPG) { let mut info_url = format!("{action_url}&{TAG_STREAM_ID}={}", pli.provider_id); @@ -485,14 +504,14 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, info_url = format!("{info_url}&limit={limit}"); } if user.proxy == ProxyType::Redirect { - return HttpResponse::Found().insert_header(("Location", info_url)).finish(); + return redirect(&info_url).into_response(); } return match request::download_text_content(Arc::clone(&app_state.http_client), input, info_url.as_str(), None).await { - Ok(content) => HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(content), + Ok(content) => (axum::http::StatusCode::OK, axum::Json(content)).into_response(), Err(err) => { error!("Failed to download epg {}", sanitize_sensitive_info(err.to_string().as_str())); - get_empty_epg_response() + get_empty_epg_response().into_response() } }; } @@ -501,10 +520,10 @@ async fn xtream_get_short_epg(app_state: &AppState, user: &ProxyUserCredentials, } } warn!("Cant find short epg with id: {target_name}/{stream_id}"); - get_empty_epg_response() + get_empty_epg_response().into_response() } -async fn xtream_player_api_handle_content_action(config: &Config, target_name: &str, action: &str, category_id: Option, user: &ProxyUserCredentials, req: &HttpRequest) -> Option { +async fn xtream_player_api_handle_content_action(config: &Config, target_name: &str, action: &str, category_id: Option, user: &ProxyUserCredentials) -> Option { if let Ok((path, content)) = match action { ACTION_GET_LIVE_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_LIVE), ACTION_GET_VOD_CATEGORIES => xtream_repository::xtream_get_collection_path(config, target_name, xtream_repository::COL_CAT_VOD), @@ -520,18 +539,20 @@ async fn xtream_player_api_handle_content_action(config: &Config, target_name: & _ => None }; if let Some(flt) = filter { - return Some(serve_query(&file_path, &HashMap::from([(TAG_CATEGORY_ID, flt)]))); + return Some(serve_query(&file_path, &HashMap::from([(TAG_CATEGORY_ID, flt)])).into_response()); } - return Some(serve_file(&file_path, req, mime::APPLICATION_JSON).await); + return Some(serve_file(&file_path, mime::APPLICATION_JSON).await.into_response()); } else if let Some(payload) = content { - return Some(HttpResponse::Ok().body(payload)); + return Some(axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .body(payload).unwrap().into_response()); } - return Some(api_utils::empty_json_list_response()); + return Some(api_utils::empty_json_list_response().into_response()); } None } -async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget, stream_id: &str, start: &str, end: &str) -> HttpResponse { +async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget, stream_id: &str, start: &str, end: &str) -> impl axum::response::IntoResponse + Send { let virtual_id: u32 = try_result_bad_request!(FromStr::from_str(stream_id)); let (pli, _) = try_result_bad_request!(xtream_repository::xtream_get_item_for_stream_id(virtual_id, &app_state.config, target, Some(XtreamCluster::Live))); let input = try_option_bad_request!(app_state.config.get_input_by_name(pli.input_name.as_str())); @@ -540,29 +561,35 @@ async fn xtream_get_catchup_response(app_state: &AppState, target: &ConfigTarget let mut doc: Map = try_result_bad_request!(serde_json::from_str(&content)); let epg_listings = try_option_bad_request!(doc.get_mut(TAG_EPG_LISTINGS).and_then(Value::as_array_mut)); let target_path = try_option_bad_request!(get_target_storage_path(&app_state.config, target.name.as_str())); - let (mut target_id_mapping, file_lock) = get_target_id_mapping(&app_state.config, &target_path); + let (mut target_id_mapping, file_lock) = get_target_id_mapping(&app_state.config, &target_path).await; for epg_list_item in epg_listings.iter_mut().filter_map(Value::as_object_mut) { // TODO epg_id if let Some(catchup_provider_id) = epg_list_item.get(TAG_ID).and_then(Value::as_str).and_then(|id| id.parse::().ok()) { let uuid = generate_playlist_uuid(&hex_encode(&pli.get_uuid()), &catchup_provider_id.to_string(), pli.item_type, &pli.url); - let virtual_id = target_id_mapping.get_and_update_virtual_id(uuid, catchup_provider_id, PlaylistItemType::Catchup, pli.provider_id); + let virtual_id = target_id_mapping.get_and_update_virtual_id(&uuid, catchup_provider_id, PlaylistItemType::Catchup, pli.provider_id); epg_list_item.insert(TAG_ID.to_string(), Value::String(virtual_id.to_string())); } } if let Err(err) = target_id_mapping.persist() { error!("Failed to write catchup id mapping {err}"); - return HttpResponse::BadRequest().finish(); + return axum::http::StatusCode::BAD_REQUEST.into_response(); } drop(file_lock); - serde_json::to_string(&doc).map_or_else(|_| HttpResponse::BadRequest().finish(), |result| HttpResponse::Ok().content_type(mime::APPLICATION_JSON).body(result)) + serde_json::to_string(&doc) + .map_or_else( + |_| axum::http::StatusCode::BAD_REQUEST.into_response(), + |result| axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(result).unwrap().into_response()) } macro_rules! skip_json_response_if_flag_set { ($flag:expr, $stmt:expr) => { if $flag { - return api_utils::empty_json_list_response(); + return api_utils::empty_json_list_response().into_response(); } - return $stmt; + return $stmt.into_response(); }; } @@ -577,23 +604,22 @@ macro_rules! skip_flag_optional { } async fn xtream_player_api( - req: &HttpRequest, api_req: UserApiRequest, - app_state: &web::Data>, -) -> HttpResponse { + app_state: &Arc, +) -> impl axum::response::IntoResponse + Send { let user_target = get_user_target(&api_req, app_state).await; if let Some((user, target)) = user_target { if !target.has_output(&TargetType::Xtream) { - return HttpResponse::Ok().json(get_user_info(&user, app_state)); + return axum::response::Json(get_user_info(&user, app_state).await).into_response(); } let action = api_req.action.trim(); if action.is_empty() { - return HttpResponse::Ok().json(get_user_info(&user, app_state)); + return axum::response::Json(get_user_info(&user, app_state).await).into_response(); } - if !user.has_permissions(app_state) { - return HttpResponse::Forbidden().finish(); + if !user.has_permissions(app_state).await { + return axum::http::StatusCode::FORBIDDEN.into_response(); } // Process specific playlist actions @@ -616,7 +642,7 @@ async fn xtream_player_api( ACTION_GET_EPG | ACTION_GET_SHORT_EPG => { return xtream_get_short_epg( app_state, &user, target, api_req.stream_id.trim(), api_req.limit.trim(), - ).await; + ).await.into_response(); } ACTION_GET_CATCHUP_TABLE => { skip_json_response_if_flag_set!(skip_live, xtream_get_catchup_response(app_state, target, api_req.stream_id.trim(), api_req.start.trim(), api_req.end.trim()).await); @@ -627,9 +653,9 @@ async fn xtream_player_api( let category_id = api_req.category_id.trim().parse::().ok(); // Handle general content actions if let Some(response) = xtream_player_api_handle_content_action( - &app_state.config, &target.name, action, category_id, &user, req, + &app_state.config, &target.name, action, category_id, &user, ).await { - return response; + return response.into_response(); } let result = match action { @@ -649,20 +675,21 @@ async fn xtream_player_api( Ok(xtream_iter) => { // Convert the iterator into a stream of `Bytes` let content_stream = xtream_create_content_stream(xtream_iter); - HttpResponse::Ok() - .content_type(mime::APPLICATION_JSON) - .streaming(content_stream) + axum::response::Response::builder() + .status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()) + .body(axum::body::Body::from_stream(content_stream)).unwrap().into_response() } Err(err) => { error!("Failed response for xtream target: {} action: {} error: {}", &target.name, action, err); // Some players fail on NoContent, so we return an empty array - api_utils::empty_json_list_response() + api_utils::empty_json_list_response().into_response() } } } None => { // Some players fail on NoContent, so we return an empty array - api_utils::empty_json_list_response() + api_utils::empty_json_list_response().into_response() } } } else { @@ -671,7 +698,7 @@ async fn xtream_player_api( (_, true) => debug!("Parameter action is empty!"), _ => debug!("Bad request!"), } - HttpResponse::BadRequest().finish() + axum::http::StatusCode::BAD_REQUEST.into_response() } } @@ -686,65 +713,90 @@ fn xtream_create_content_stream(xtream_iter: impl Iterator) })).chain(stream::once(async { Ok::(Bytes::from("]")) }))) } -async fn xtream_player_api_get(req: HttpRequest, - api_req: web::Query, - app_state: web::Data>, -) -> HttpResponse { - xtream_player_api(&req, api_req.into_inner(), &app_state).await +#[axum::debug_handler] +async fn xtream_player_api_get( + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Query(api_req): axum::extract::Query, +) -> impl axum::response::IntoResponse + Send { + xtream_player_api(api_req, &app_state).await } -async fn xtream_player_api_post(req: HttpRequest, - api_req: web::Form, - app_state: web::Data>, -) -> HttpResponse { - xtream_player_api(&req, api_req.into_inner(), &app_state).await +// async fn xtream_player_api_get(req: axum::http::Request, +// axum::extract::Query(api_req): axum::extract::Query, +// axum::extract::State(app_state): axum::extract::State>, +// ) -> impl axum::response::IntoResponse + Send { +// xtream_player_api(&req, api_req.into_inner(), &app_state).await +// } +#[axum::debug_handler] +async fn xtream_player_api_post( + axum::extract::State(app_state): axum::extract::State>, + axum::extract::Form(api_req): axum::extract::Form, +) -> impl axum::response::IntoResponse + Send { + xtream_player_api(api_req, &app_state).await } +// async fn xtream_player_api_post(req: axum::http::Request, +// api_req: web::Form, +// axum::extract::State(app_state): axum::extract::State>, +// ) -> impl axum::response::IntoResponse + Send { +// xtream_player_api(&req, api_req.into_inner(), &app_state).await +// } macro_rules! register_xtream_api { - ($cfg:expr, [$($path:expr),*]) => {{ + ($router:expr, [$($path:expr),*]) => {{ + $router $( - $cfg.service(web::resource($path).route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_post))); + .route($path, axum::routing::get(xtream_player_api_get)) + .route($path, axum::routing::post(xtream_player_api_post)) + // $router.service(web::resource($path).route(web::get().to(xtream_player_api_get)).route(web::post().to(xtream_player_api_post))) )* }}; } macro_rules! register_xtream_api_stream { - ($cfg:expr, [$(($path:expr, $fn_name:ident)),*]) => {{ + ($router:expr, [$(($path:expr, $fn_name:ident)),*]) => {{ + $router $( - $cfg.service(web::resource(format!("{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to($fn_name))); + .route(format!("{}/{{username}}/{{password}}/{{stream_id}}", $path).as_str(), axum::routing::get($fn_name)) + // $cfg.service(web::resource(format!("{}/{{username}}/{{password}}/{{stream_id}}", $path)).route(web::get().to($fn_name))); )* }}; } macro_rules! register_xtream_api_resource { - ($cfg:expr, [$(($path:expr, $fn_name:ident)),*]) => {{ + ($router:expr, [$(($path:expr, $fn_name:ident)),*]) => {{ + $router $( - $cfg.service(web::resource(format!("/resource/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", $path)).route(web::get().to($fn_name))); + .route(format!("/resource/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", $path).as_str(), axum::routing::get($fn_name)) + // $cfg.service(web::resource(format!("/resource/{}/{{username}}/{{password}}/{{stream_id}}/{{resource}}", $path)).route(web::get().to($fn_name))); )* }}; } macro_rules! register_xtream_api_timeshift { - ($cfg:expr, [$($path:expr),*]) => {{ + ($router:expr, [$($path:expr),*]) => {{ + $router $( - $cfg.service(web::resource($path).route(web::get().to(xtream_player_api_timeshift_stream)).route(web::post().to(xtream_player_api_timeshift_stream))); + .route($path, axum::routing::get(xtream_player_api_timeshift_stream)) + .route($path, axum::routing::post(xtream_player_api_timeshift_stream)) + //$cfg.service(web::resource($path).route(web::get().to(xtream_player_api_timeshift_stream)).route(web::post().to(xtream_player_api_timeshift_stream))); )* }}; } -pub fn xtream_api_register(cfg: &mut web::ServiceConfig) { - register_xtream_api!(cfg, ["/player_api.php", "/panel_api.php", "/xtream"]); - register_xtream_api_stream!(cfg, [ +pub fn xtream_api_register() -> axum::Router> { + let router = axum::Router::new(); + let mut router = register_xtream_api!(router, ["/player_api.php", "/panel_api.php", "/xtream"]); + router = register_xtream_api_stream!(router, [ ("", xtream_player_api_live_stream_alt), ("/live", xtream_player_api_live_stream), ("/movie", xtream_player_api_movie_stream), ("/series", xtream_player_api_series_stream)]); - register_xtream_api_timeshift!(cfg, [ + router = register_xtream_api_timeshift!(router, [ "/timeshift/{username}/{password}/{duration}/{start}/{stream_id}", "/timeshift.php", "/streaming/timeshift.php"]); - register_xtream_api_resource!(cfg, [ + register_xtream_api_resource!(router, [ ("live", xtream_player_api_live_resource), ("movie", xtream_player_api_movie_resource), - ("series", xtream_player_api_series_resource)]); + ("series", xtream_player_api_series_resource)]) } \ No newline at end of file diff --git a/src/api/main_api.rs b/src/api/main_api.rs index 518e17f27..e51f9817f 100644 --- a/src/api/main_api.rs +++ b/src/api/main_api.rs @@ -1,34 +1,32 @@ -use actix_cors::Cors; -use actix_web::middleware::Logger; -use actix_web::web::Data; -use actix_web::{web, App, HttpResponse, HttpServer}; -use parking_lot::{Mutex as PlMutex}; -use log::{error, info}; -use std::io::ErrorKind; -use std::path::PathBuf; -use std::sync::Arc; -use chrono::{DateTime, Utc}; -use mime::APPLICATION_JSON; +use crate::api::endpoints::hdhomerun_api::hdhr_api_register; use crate::api::endpoints::hls_api::hls_api_register; use crate::api::endpoints::m3u_api::m3u_api_register; -use crate::api::model::app_state::{AppState, HdHomerunAppState}; -use crate::api::model::download::DownloadQueue; -use crate::api::model::streams::shared_stream_manager::SharedStreamManager; -use crate::api::scheduler::start_scheduler; use crate::api::endpoints::v1_api::v1_api_register; use crate::api::endpoints::web_index::index_register; use crate::api::endpoints::xmltv_api::xmltv_api_register; use crate::api::endpoints::xtream_api::xtream_api_register; +use crate::api::model::active_provider_manager::ActiveProviderManager; use crate::api::model::active_user_manager::ActiveUserManager; +use crate::api::model::app_state::{AppState, HdHomerunAppState}; +use crate::api::model::download::DownloadQueue; +use crate::api::model::streams::shared_stream_manager::SharedStreamManager; +use crate::api::scheduler::start_scheduler; use crate::model::config::{validate_targets, Config, ProcessTargets, ScheduleConfig}; use crate::model::healthcheck::Healthcheck; use crate::processing::processor::playlist; -use crate::tools::lru_cache::{LRUResourceCache}; +use crate::tools::lru_cache::LRUResourceCache; use crate::utils::size_utils::human_readable_byte_size; use crate::utils::sys_utils; use crate::{BUILD_TIMESTAMP, VERSION}; -use crate::api::endpoints::hdhomerun_api::{hdhr_api_register}; -use crate::api::model::active_provider_manager::ActiveProviderManager; +use axum::response::IntoResponse; +use chrono::{DateTime, Utc}; +use log::{error, info}; +use std::io::ErrorKind; +use std::path::PathBuf; +use std::sync::Arc; +use axum::debug_handler; +use tokio::sync::Mutex; +use std::future::IntoFuture; fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result { let web_dir = web_root.to_string(); @@ -40,12 +38,17 @@ fn get_web_dir_path(web_ui_enabled: bool, web_root: &str) -> Result>) -> Healthcheck { +async fn create_healthcheck(app_state: &Arc) -> Healthcheck { let server_time = chrono::offset::Local::now().with_timezone(&chrono::Local).format("%Y-%m-%d %H:%M:%S %Z").to_string(); - let cache = app_state.cache.as_ref().as_ref().map(|c| c.lock().get_size_text()); - let (active_clients, active_connections) = { + let cache = match app_state.cache.as_ref().as_ref() { + None => None, + Some(lock) => { + Some(lock.lock().await.get_size_text()) + } + }; + let (active_clients, active_connections) = { let active_user = &app_state.active_users; - (active_user.active_users(), active_user.active_connections()) + (active_user.active_users().await, active_user.active_connections().await) }; let build_time: Option = BUILD_TIMESTAMP.to_string().parse::>().ok().map(|datetime| datetime.format("%Y-%m-%d %H:%M:%S %Z").to_string()); Healthcheck { @@ -60,27 +63,29 @@ fn create_healthcheck(app_state: &web::Data>) -> Healthcheck { } } -async fn healthcheck(app_state: web::Data>,) -> HttpResponse { - HttpResponse::Ok().json(create_healthcheck(&app_state)) +#[debug_handler] +async fn healthcheck(axum::extract::State(app_state): axum::extract::State>) -> impl axum::response::IntoResponse { + axum::Json(create_healthcheck(&app_state).await) } -async fn status(app_state: web::Data>,) -> HttpResponse { - let status = create_healthcheck(&app_state); +async fn status(axum::extract::State(app_state): axum::extract::State>) -> impl axum::response::IntoResponse { + let status = create_healthcheck(&app_state).await; match serde_json::to_string_pretty(&status) { - Ok(pretty_json) => HttpResponse::Ok().content_type(APPLICATION_JSON).body(pretty_json), - Err(_) => HttpResponse::Ok().json(status), + Ok(pretty_json) => axum::response::Response::builder().status(axum::http::StatusCode::OK) + .header(axum::http::header::CONTENT_TYPE, mime::APPLICATION_JSON.to_string()).body(pretty_json).unwrap().into_response(), + Err(_) => axum::Json(status).into_response(), } } fn create_shared_data(cfg: &Arc) -> AppState { - let lru_cache = cfg.reverse_proxy.as_ref().and_then(|r| r.cache.as_ref()).and_then(|c| if c.enabled { - Some(PlMutex::new(LRUResourceCache::new(c.t_size, &PathBuf::from(c.dir.as_ref().unwrap())))) - } else { None} ); + let lru_cache = cfg.reverse_proxy.as_ref().and_then(|r| r.cache.as_ref()).and_then(|c| if c.enabled { + Some(Mutex::new(LRUResourceCache::new(c.t_size, &PathBuf::from(c.dir.as_ref().unwrap())))) + } else { None }); let cache = Arc::new(lru_cache); let cache_scanner = Arc::clone(&cache); - actix_rt::spawn(async move { + tokio::spawn(async move { if let Some(m) = cache_scanner.as_ref() { - let mut c = m.lock(); + let mut c = m.lock().await; if let Err(err) = (*c).scan() { error!("Failed to scan cache {err}"); } @@ -102,7 +107,7 @@ fn exec_update_on_boot(client: Arc, cfg: &Arc, targets: if cfg.update_on_boot { let cfg_clone = Arc::clone(cfg); let targets_clone = Arc::clone(targets); - actix_rt::spawn( + tokio::spawn( async move { playlist::exec_processing(client, cfg_clone, targets_clone).await } ); } @@ -145,7 +150,7 @@ fn exec_scheduler(client: &Arc, cfg: &Arc, targets: &Ar let exec_targets = get_process_targets(cfg, targets, schedule.targets.as_ref()); let cfg_clone = Arc::clone(cfg); let http_client = Arc::clone(client); - actix_rt::spawn(async move { + tokio::spawn(async move { start_scheduler(http_client, expression.as_str(), cfg_clone, exec_targets).await; }); } @@ -171,22 +176,32 @@ fn start_hdhomerun(cfg: &Arc, app_state: &Arc, infos: &mut Vec let port = device.port; let device_clone = Arc::new(device.clone()); infos.push(format!("HdHomeRun Server '{}' running: http://{host}:{port}", device.name)); - actix_rt::spawn(async move { - HttpServer::new(move || { - App::new() - .wrap(Logger::default()) - .wrap(Cors::default() - .supports_credentials() - .allow_any_origin() - .allowed_methods(vec!["GET", "POST", "OPTIONS", "HEAD"]) - .allow_any_header() - .max_age(3600)) - .app_data(Data::new(HdHomerunAppState { - app_state: Arc::clone(&app_data), - device: Arc::clone(&device_clone), - })) - .configure(hdhr_api_register) - }).bind(format!("{}:{port}", app_host.clone()))?.run().await + tokio::spawn(async move { + let cors = tower_http::cors::CorsLayer::new() + // .allow_credentials(true) + .allow_origin(tower_http::cors::Any) + .allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::OPTIONS, axum::http::Method::HEAD]) + .allow_headers(tower_http::cors::Any) + .max_age(std::time::Duration::from_secs(3600)); + + let router = axum::Router::>::new() + .layer(cors) + // .layer(TraceLayer::new_for_http()) // `Logger::default()` + .merge(hdhr_api_register()); + + let router: axum::Router<()> = router.with_state(Arc::new(HdHomerunAppState { + app_state: Arc::clone(&app_data), + device: Arc::clone(&device_clone), + })); + + match tokio::net::TcpListener::bind(format!("{}:{}", app_host.clone(), port)).await { + Ok(listener) => { + if let Err(err) = axum::serve(listener, router).into_future().await { + error!("{err}"); + } + }, + Err(err) => error!("{err}"), + } }); } } @@ -194,7 +209,6 @@ fn start_hdhomerun(cfg: &Arc, app_state: &Arc, infos: &mut Vec } } -#[actix_web::main] pub async fn start_server(cfg: Arc, targets: Arc) -> futures::io::Result<()> { let mut infos = Vec::new(); let host = cfg.api.host.to_string(); @@ -208,13 +222,13 @@ pub async fn start_server(cfg: Arc, targets: Arc) -> fut infos.push(format!("Web root: {:?}", &web_dir_path)); } let app_state = Arc::new(create_shared_data(&cfg)); - let shared_data = Data::new(Arc::clone(&app_state)); + let shared_data = Arc::clone(&app_state); exec_scheduler(&Arc::clone(&shared_data.http_client), &cfg, &targets); exec_update_on_boot(Arc::clone(&shared_data.http_client), &cfg, &targets); let web_auth_enabled = is_web_auth_enabled(&cfg, web_ui_enabled); - if cfg.t_api_proxy.read().is_some() { + if cfg.t_api_proxy.read().await.is_some() { start_hdhomerun(&cfg, &app_state, &mut infos); } @@ -222,34 +236,66 @@ pub async fn start_server(cfg: Arc, targets: Arc) -> fut for info in &infos { info!("{info}"); } + // Web Server - HttpServer::new(move || { - App::new() - .wrap(Logger::default()) - .wrap(Cors::default() - .supports_credentials() - .allow_any_origin() - .allowed_methods(vec!["GET", "POST", "OPTIONS", "HEAD"]) - .allow_any_header() - .max_age(3600)) - .app_data(shared_data.clone()) - // .wrap(Condition::new(web_auth_enabled, ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized))) - .configure(|srvcfg| { - if web_ui_enabled { - srvcfg.service(actix_files::Files::new("/static", web_dir_path.join("static"))); - srvcfg.configure(v1_api_register(web_auth_enabled)); - } - srvcfg.service(web::resource("/healthcheck").route(web::get().to(healthcheck))); - srvcfg.service(web::resource("/status").route(web::get().to(status))); - }) - .configure(xtream_api_register) - .configure(m3u_api_register) - .configure(xmltv_api_register) - .configure(hls_api_register) - .configure(|srvcfg| { - if web_ui_enabled { - srvcfg.configure(index_register(&web_dir_path)); - } - }) - }).bind(format!("{host}:{port}"))?.run().await + let cors = tower_http::cors::CorsLayer::new() + // .allow_credentials(true) + .allow_origin(tower_http::cors::Any) + .allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::OPTIONS, axum::http::Method::HEAD]) + .allow_headers(tower_http::cors::Any) + .max_age(std::time::Duration::from_secs(3600)); + + let mut router = axum::Router::new() + .layer(cors) + // .layer(TraceLayer::new_for_http()) // `Logger::default()` + .route("/healthcheck", axum::routing::get(healthcheck)) + .route("/status", axum::routing::get(status)); + if web_ui_enabled { + router = router + .nest_service("/static", tower_http::services::ServeDir::new(web_dir_path.join("static"))) + .merge(v1_api_register(web_auth_enabled, Arc::clone(&shared_data))); + } + router = router + .merge(xtream_api_register()) + .merge(m3u_api_register()) + .merge(xmltv_api_register()) + .merge(hls_api_register()); + + if web_ui_enabled { + router = router.merge(index_register(&web_dir_path)); + } + + let router: axum::Router<()> = router.with_state(shared_data.clone()); + let listener = tokio::net::TcpListener::bind(format!("{host}:{port}")).await?; + axum::serve(listener, router).into_future().await + + // HttpServer::new(move || { + // App::new() + // .wrap(Logger::default()) + // .wrap(Cors::default() + // .supports_credentials() + // .allow_any_origin() + // .allowed_methods(vec!["GET", "POST", "OPTIONS", "HEAD"]) + // .allow_any_header() + // .max_age(3600)) + // .app_data(shared_data.clone()) + // // .wrap(Condition::new(web_auth_enabled, ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, handle_unauthorized))) + // .configure(|srvcfg| { + // if web_ui_enabled { + // srvcfg.service(actix_files::Files::new("/static", web_dir_path.join("static"))); + // srvcfg.configure(v1_api_register(web_auth_enabled)); + // } + // srvcfg.service(web::resource("/healthcheck").route(web::get().to(healthcheck))); + // srvcfg.service(web::resource("/status").route(web::get().to(status))); + // }) + // .configure(xtream_api_register) + // .configure(m3u_api_register) + // .configure(xmltv_api_register) + // .configure(hls_api_register) + // .configure(|srvcfg| { + // if web_ui_enabled { + // srvcfg.configure(index_register(&web_dir_path)); + // } + // }) + // }).bind(format!("{host}:{port}"))?.run().await } diff --git a/src/api/model/active_provider_manager.rs b/src/api/model/active_provider_manager.rs index 36f21490d..6e1d6ac23 100644 --- a/src/api/model/active_provider_manager.rs +++ b/src/api/model/active_provider_manager.rs @@ -1,7 +1,7 @@ use crate::model::config::{ConfigInput, ConfigInputAlias, InputType}; -use parking_lot::RwLock; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; +use tokio::sync::RwLock; /// This struct represents an individual provider configuration with fields like: /// @@ -53,8 +53,8 @@ impl ProviderConfig { } #[inline] - pub fn is_exhausted(&self) -> bool { - self.max_connections > 0 && *self.current_connections.read() >= self.max_connections + pub async fn is_exhausted(&self) -> bool { + self.max_connections > 0 && *self.current_connections.read().await >= self.max_connections } // // #[inline] @@ -62,8 +62,8 @@ impl ProviderConfig { // !self.is_exhausted() // } - pub fn try_allocate(&self, force: bool) -> bool { - let mut connections = self.current_connections.write(); + pub async fn try_allocate(&self, force: bool) -> bool { + let mut connections = self.current_connections.write().await; if force || *connections < self.max_connections { *connections += 1; return true; @@ -71,8 +71,8 @@ impl ProviderConfig { false } - pub fn release(&self) { - let mut connections = self.current_connections.write(); + pub async fn release(&self) { + let mut connections = self.current_connections.write().await; if *connections > 0 { *connections -= 1; } @@ -90,17 +90,17 @@ enum ProviderLineup { } impl ProviderLineup { - fn acquire(&self, force: bool) -> Option<&ProviderConfig> { + async fn acquire(&self, force: bool) -> Option<&ProviderConfig> { match self { - ProviderLineup::Single(lineup) => lineup.acquire(force), - ProviderLineup::Multi(lineup) => lineup.acquire(force), + ProviderLineup::Single(lineup) => lineup.acquire(force).await, + ProviderLineup::Multi(lineup) => lineup.acquire(force).await, } } - fn release(&self, provider_id: u16) { + async fn release(&self, provider_id: u16) { match self { - ProviderLineup::Single(lineup) => lineup.release(provider_id), - ProviderLineup::Multi(lineup) => lineup.release(provider_id), + ProviderLineup::Single(lineup) => lineup.release(provider_id).await, + ProviderLineup::Multi(lineup) => lineup.release(provider_id).await, } } } @@ -118,17 +118,17 @@ impl SingleProviderLineup { } } - fn acquire(&self, force: bool) -> Option<&ProviderConfig> { - if self.provider.try_allocate(force) { + async fn acquire(&self, force: bool) -> Option<&ProviderConfig> { + if self.provider.try_allocate(force).await { Some(&self.provider) } else { None } } - fn release(&self, provider_id: u16) { + async fn release(&self, provider_id: u16) { if self.provider.id == provider_id { - self.provider.release(); + self.provider.release().await; } } } @@ -145,10 +145,17 @@ enum ProviderPriorityGroup { } impl ProviderPriorityGroup { - fn is_exhausted(&self) -> bool { + async fn is_exhausted(&self) -> bool { match self { - ProviderPriorityGroup::SingleProviderGroup(g) => g.is_exhausted(), - ProviderPriorityGroup::MultiProviderGroup(_, groups) => groups.iter().all(ProviderConfig::is_exhausted) + ProviderPriorityGroup::SingleProviderGroup(g) => g.is_exhausted().await, + ProviderPriorityGroup::MultiProviderGroup(_, groups) => { + for g in groups { + if !g.is_exhausted().await { + return false; + } + } + true + } } } } @@ -221,10 +228,10 @@ impl MultiProviderLineup { /// println!("No available providers in group 0."); /// } /// ``` - fn acquire_next_provider_from_group(priority_group: &ProviderPriorityGroup) -> Option<&ProviderConfig> { + async fn acquire_next_provider_from_group(priority_group: &ProviderPriorityGroup) -> Option<&ProviderConfig> { match priority_group { ProviderPriorityGroup::SingleProviderGroup(p) => { - if p.try_allocate(false) { + if p.try_allocate(false).await { return Some(p); } } @@ -234,7 +241,7 @@ impl MultiProviderLineup { for _ in 0..provider_count { let p = pg.get(idx).unwrap(); idx = (idx + 1) % provider_count; - if p.try_allocate(false) { + if p.try_allocate(false).await { index.store(idx, Ordering::SeqCst); return Some(p); } @@ -275,15 +282,15 @@ impl MultiProviderLineup { /// println!("No available providers."); /// } /// ``` - fn acquire(&self, force: bool) -> Option<&ProviderConfig> { + async fn acquire(&self, force: bool) -> Option<&ProviderConfig> { let mut main_idx = self.index.load(Ordering::SeqCst); let provider_count = self.providers.len(); for _ in 0..provider_count { let priority_group = &self.providers[main_idx]; main_idx = (main_idx + 1) % provider_count; - if let Some(provider) = Self::acquire_next_provider_from_group(priority_group) { - if priority_group.is_exhausted() { + if let Some(provider) = Self::acquire_next_provider_from_group(priority_group).await { + if priority_group.is_exhausted().await { self.index.store(main_idx, Ordering::SeqCst); } return Some(provider); @@ -308,19 +315,19 @@ impl MultiProviderLineup { } - fn release(&self, provider_id: u16) { + async fn release(&self, provider_id: u16) { for g in &self.providers { match g { ProviderPriorityGroup::SingleProviderGroup(pc) => { if pc.id == provider_id { - pc.release(); + pc.release().await; break; } } ProviderPriorityGroup::MultiProviderGroup(_, group) => { for pc in group { if pc.id == provider_id { - pc.release(); + pc.release().await; return; } } @@ -328,6 +335,7 @@ impl MultiProviderLineup { } } } + } pub struct ActiveProviderManager { @@ -352,13 +360,16 @@ impl ActiveProviderManager { self.providers.insert(name.to_string(), lineup); } - pub fn acquire_connection(&self, lineup_name: &str) -> Option<&ProviderConfig> { - self.providers.get(lineup_name).and_then(|lineup| lineup.acquire(self.user_access_control)) + pub async fn acquire_connection(&self, lineup_name: &str) -> Option<&ProviderConfig> { + match self.providers.get(lineup_name) { + None => None, + Some(lineup) => lineup.acquire(self.user_access_control).await + } } - pub fn release_connection(&self, lineup_name: &str, provider_id: u16) { + pub async fn release_connection(&self, lineup_name: &str, provider_id: u16) { if let Some(lineup) = self.providers.get(lineup_name) { - lineup.release(provider_id); + lineup.release(provider_id).await; } } } diff --git a/src/api/model/active_user_manager.rs b/src/api/model/active_user_manager.rs index c8b4cd91a..b0d6a29d0 100644 --- a/src/api/model/active_user_manager.rs +++ b/src/api/model/active_user_manager.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicU32, Ordering}; -use parking_lot::RwLock; +use tokio::sync::RwLock; pub struct ActiveUserManager { pub user: RwLock>, @@ -19,44 +19,40 @@ impl ActiveUserManager { } } - pub fn user_connections(&self, username: &str) -> u32 { - if let Some(counter) = self.user.read().get(username) { + pub async fn user_connections(&self, username: &str) -> u32 { + if let Some(counter) = self.user.read().await.get(username) { return counter.load(std::sync::atomic::Ordering::SeqCst); } 0 } - pub fn active_users(&self) -> usize { - self.user.read().len() + pub async fn active_users(&self) -> usize { + self.user.read().await.len() } - pub fn active_connections(&self) -> usize { - self.user.read().values().map(|c| c.load(Ordering::SeqCst) as usize).sum() + pub async fn active_connections(&self) -> usize { + self.user.read().await.values().map(|c| c.load(Ordering::SeqCst) as usize).sum() } - pub fn add_connection(&self, username: &str) -> (usize, usize) { - { - let mut lock = self.user.write(); - if let Some(counter) = lock.get(username) { - counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - } else { - lock.insert(username.to_string(), AtomicU32::new(1)); - } - drop(lock); + pub async fn add_connection(&self, username: &str) -> (usize, usize) { + let mut lock = self.user.write().await; + if let Some(counter) = lock.get(username) { + counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + } else { + lock.insert(username.to_string(), AtomicU32::new(1)); } - (self.active_users(), self.active_connections()) + drop(lock); + (self.active_users().await, self.active_connections().await) } - pub fn remove_connection(&self, username: &str) -> (usize, usize) { - { - let mut lock = self.user.write(); - if let Some(counter) = lock.get(username) { - if counter.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) == 1 { - lock.remove(username); - } + pub async fn remove_connection(&self, username: &str) -> (usize, usize) { + let mut lock = self.user.write().await; + if let Some(counter) = lock.get(username) { + if counter.fetch_sub(1, std::sync::atomic::Ordering::SeqCst) == 1 { + lock.remove(username); } - drop(lock); } - (self.active_users(), self.active_connections()) + drop(lock); + (self.active_users().await, self.active_connections().await) } } \ No newline at end of file diff --git a/src/api/model/app_state.rs b/src/api/model/app_state.rs index c56a2e6e7..bba5ed6be 100644 --- a/src/api/model/app_state.rs +++ b/src/api/model/app_state.rs @@ -1,5 +1,5 @@ -use std::sync::{Arc}; -use parking_lot::{Mutex}; +use tokio::sync::{Mutex}; +use std::sync::Arc; use crate::api::model::active_provider_manager::ActiveProviderManager; use crate::api::model::active_user_manager::ActiveUserManager; use crate::api::model::download::DownloadQueue; @@ -8,6 +8,8 @@ use crate::model::config::{Config}; use crate::model::hdhomerun_config::HdHomeRunDeviceConfig; use crate::tools::lru_cache::LRUResourceCache; + +#[derive(Clone)] pub struct AppState { pub config: Arc, pub http_client: Arc, @@ -19,11 +21,12 @@ pub struct AppState { } impl AppState { - pub fn get_active_connections_for_user(&self, username: &str) -> u32 { - self.active_users.user_connections(username) + pub async fn get_active_connections_for_user(&self, username: &str) -> u32 { + self.active_users.user_connections(username).await } } +#[derive(Clone)] pub struct HdHomerunAppState { pub app_state: Arc, pub device: Arc, diff --git a/src/api/model/download.rs b/src/api/model/download.rs index 0db5e1572..ed9700263 100644 --- a/src/api/model/download.rs +++ b/src/api/model/download.rs @@ -3,7 +3,6 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use tokio::sync::{RwLock, Mutex}; use std::sync::Arc; -use actix_web::web; use serde::{Deserialize, Serialize}; use unidecode::unidecode; @@ -141,9 +140,3 @@ pub struct FileDownloadRequest { pub url: String, pub filename: String, } - -impl From> for FileDownloadRequest { - fn from(req: web::Json) -> Self { - req.clone() - } -} diff --git a/src/api/model/model_utils.rs b/src/api/model/model_utils.rs index b057dfb04..059e40cc8 100644 --- a/src/api/model/model_utils.rs +++ b/src/api/model/model_utils.rs @@ -1,6 +1,4 @@ use crate::utils::debug_if_enabled; -use actix_web::http::header::{HeaderName, HeaderValue}; -use actix_web::{HttpResponseBuilder}; use reqwest::{StatusCode}; use std::collections::{HashSet}; use std::str::FromStr; @@ -16,35 +14,45 @@ pub fn get_response_headers(headers: &HeaderMap) -> Vec<(String, String)> { response_headers } -pub fn get_stream_response_with_headers(custom: Option<(Vec<(String, String)>, StatusCode)>, stream_url: &str) -> HttpResponseBuilder { - let mut headers = Vec::<(HeaderName, HeaderValue)>::with_capacity(12); +pub fn get_stream_response_with_headers(custom: Option<(Vec<(String, String)>, StatusCode)>, stream_url: &str) -> (axum::http::StatusCode, axum::http::HeaderMap) { + let mut headers = HeaderMap::new(); let mut added_headers: HashSet = HashSet::new(); - let mut status = 200_u16; + let mut status = StatusCode::OK; + if let Some((custom_headers, status_code)) = custom { - status = status_code.as_u16(); - for header in custom_headers { - headers.push((HeaderName::from_str(&header.0).unwrap(), HeaderValue::from_str(header.1.as_str()).unwrap())); - added_headers.insert(header.0.to_string()); + status = status_code; + for (key, value) in custom_headers { + if let (Ok(name), Ok(val)) = (axum::http::HeaderName::from_str(&key), axum::http::HeaderValue::from_str(&value)) { + headers.insert(name.clone(), val); + added_headers.insert(key); + } } } let default_headers = vec![ - (actix_web::http::header::CONTENT_TYPE, HeaderValue::from_str("application/octet-stream").unwrap()), - (actix_web::http::header::CONNECTION, HeaderValue::from_str("keep-alive").unwrap()), + ("content-type", "application/octet-stream"), + ("connection", "keep-alive"), ]; - for header in default_headers { - if !added_headers.contains(header.0.as_str()) { - headers.push(header); + for (key, value) in default_headers { + if !added_headers.contains(key) { + if let (Ok(name), Ok(val)) = (axum::http::HeaderName::from_str(key), axum::http::HeaderValue::from_str(value)) { + headers.insert(name, val); + } } } - headers.push((actix_web::http::header::DATE, HeaderValue::from_str(&chrono::Utc::now().to_rfc2822()).unwrap())); - - let mut response_builder = actix_web::HttpResponse::build(actix_web::http::StatusCode::from_u16(status).unwrap()); - debug_if_enabled!("Responding stream request {} with status {status}, headers {headers:?}", sanitize_sensitive_info(stream_url)); - for header in headers { - response_builder.insert_header(header); + // Füge das aktuelle Datum hinzu + if let Ok(date_header) = axum::http::HeaderValue::from_str(&chrono::Utc::now().to_rfc2822()) { + headers.insert(axum::http::HeaderName::from_static("date"), date_header); } - response_builder + + debug_if_enabled!( + "Responding stream request {} with status {}, headers {:?}", + sanitize_sensitive_info(stream_url), + status, + headers + ); + + (status, headers) } \ No newline at end of file diff --git a/src/api/model/request.rs b/src/api/model/request.rs index a91b43f45..ef746358c 100644 --- a/src/api/model/request.rs +++ b/src/api/model/request.rs @@ -1,4 +1,3 @@ -use actix_web::web; use serde::de::{self, Deserializer, Unexpected}; use serde::{Deserialize, Serialize}; @@ -43,12 +42,6 @@ pub struct PlaylistRequest { pub source_name: Option, } -impl From> for PlaylistRequest { - fn from(req: web::Json) -> Self { - req.clone() - } -} - #[derive(Debug, serde::Serialize, serde::Deserialize, Default)] pub struct UserApiRequest { #[serde(default)] diff --git a/src/api/model/stream_error.rs b/src/api/model/stream_error.rs index db78d4396..737fb09cd 100644 --- a/src/api/model/stream_error.rs +++ b/src/api/model/stream_error.rs @@ -6,6 +6,7 @@ pub enum StreamError { // StdIo(std::io::Error), // ReceiverClosed, ReceiverError(BroadcastStreamRecvError), + LockError(String) } impl StreamError { @@ -26,6 +27,7 @@ impl std::fmt::Display for StreamError { // StreamError::StdIo(e) => write!(f, "IO error: {e}"), // StreamError::ReceiverClosed => write!(f, "Receiver closed"), StreamError::ReceiverError(e) => write!(f, "Receiver error {e}"), + StreamError::LockError(e) => write!(f, "{e}"), } } } \ No newline at end of file diff --git a/src/api/model/streams/active_client_stream.rs b/src/api/model/streams/active_client_stream.rs index b75227318..b9930ea82 100644 --- a/src/api/model/streams/active_client_stream.rs +++ b/src/api/model/streams/active_client_stream.rs @@ -17,8 +17,8 @@ pub(in crate::api) struct ActiveClientStream { } impl ActiveClientStream { - pub(crate) fn new(inner: ResponseStream, active_clients: Arc, user: &ProxyUserCredentials, log_active_clients: bool) -> Self { - let (client_count, connection_count) = active_clients.add_connection(&user.username); + pub(crate) async fn new(inner: ResponseStream, active_clients: Arc, user: &ProxyUserCredentials, log_active_clients: bool) -> Self { + let (client_count, connection_count) = active_clients.add_connection(&user.username).await; if log_active_clients { info!("Active clients: {client_count}, active connections {connection_count}"); } @@ -36,9 +36,16 @@ impl Stream for ActiveClientStream { impl Drop for ActiveClientStream { fn drop(&mut self) { - let (client_count, connection_count) = self.active_clients.remove_connection(&self.username); - if self.log_active_clients { - info!("Active clients: {client_count}, active connections {connection_count}"); - } + let username = self.username.clone(); + let log_active_clients = self.log_active_clients; + let active_clients = Arc::clone(&self.active_clients); + + tokio::spawn(async move { + let username = username.clone(); + let (client_count, connection_count) = active_clients.remove_connection(&username).await; + if log_active_clients { + info!("Active clients: {client_count}, active connections {connection_count}"); + } + }); } } \ No newline at end of file diff --git a/src/api/model/streams/buffered_stream.rs b/src/api/model/streams/buffered_stream.rs index f6200d470..edec33ed1 100644 --- a/src/api/model/streams/buffered_stream.rs +++ b/src/api/model/streams/buffered_stream.rs @@ -17,7 +17,7 @@ pub(in crate::api::model) struct BufferedStream { impl BufferedStream { pub fn new(stream: ResponseStream, buffer_size: usize, client_close_signal: Arc, _url: &str) -> Self { let (tx, rx) = channel(buffer_size); - actix_rt::spawn(Self::buffer_stream(tx, stream, client_close_signal)); + tokio::spawn(Self::buffer_stream(tx, stream, client_close_signal)); Self { stream: ReceiverStream::new(rx) } @@ -45,7 +45,7 @@ impl BufferedStream { } Some(Err(err)) => { trace!("Buffered Stream Error: {err:?}"); - // actix_web::rt::time::sleep(sleep_duration).await; + // tokio::time::sleep(sleep_duration).await; // Attempt to send error to client if tx.send(Err(err)).await.is_err() { client_close_signal.notify(); diff --git a/src/api/model/streams/persist_pipe_stream.rs b/src/api/model/streams/persist_pipe_stream.rs index 525c1b9a7..f518b4f2d 100644 --- a/src/api/model/streams/persist_pipe_stream.rs +++ b/src/api/model/streams/persist_pipe_stream.rs @@ -1,5 +1,6 @@ use std::io::Write; use std::pin::Pin; +use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::task::{Context, Poll}; use bytes::Bytes; @@ -26,7 +27,7 @@ pub struct PersistPipeStream { completed: bool, writer: W, size: AtomicUsize, - callback: Box, + callback: Arc, } impl PersistPipeStream @@ -39,7 +40,7 @@ where /// - `inner`: The input stream providing the data. /// - `writer`: The writer to which the data is written. /// - `callback`: A callback function to be called with the total size upon stream completion. - pub fn new(inner: S, writer: W, callback: Box) -> Self { + pub fn new(inner: S, writer: W, callback: Arc) -> Self { Self { inner, completed: false, diff --git a/src/api/model/streams/provider_stream.rs b/src/api/model/streams/provider_stream.rs index b8a865f56..2e3079a65 100644 --- a/src/api/model/streams/provider_stream.rs +++ b/src/api/model/streams/provider_stream.rs @@ -7,13 +7,13 @@ use crate::model::config::{Config, ConfigInput}; use crate::model::playlist::PlaylistItemType; use crate::utils::debug_if_enabled; use crate::utils::network::request::{get_request_headers, sanitize_sensitive_info}; -use actix_web::HttpRequest; use bytes::Bytes; use futures::stream::BoxStream; use futures::TryStreamExt; use log::{debug, error}; use reqwest::StatusCode; use std::sync::Arc; +use axum::http::HeaderMap; use url::Url; type BoxedProviderStream = BoxStream<'static, Result>; @@ -45,11 +45,11 @@ pub fn get_header_filter_for_item_type(item_type: PlaylistItemType) -> HeaderFil pub async fn get_provider_pipe_stream(cfg: &Config, http_client: &Arc, stream_url: &Url, - req: &HttpRequest, + req_headers: &HeaderMap, input: Option<&ConfigInput>, item_type: PlaylistItemType) -> ProviderStreamResponse { let filter_header = get_header_filter_for_item_type(item_type); - let req_headers = get_headers_from_request(req, &filter_header); + let req_headers = get_headers_from_request(req_headers, &filter_header); debug_if_enabled!("Stream requested with headers: {:?}", req_headers.iter().map(|header| (header.0, String::from_utf8_lossy(header.1))).collect::>()); // These are the configured headers for this input. let input_headers = input.map(|i| i.headers.clone()); @@ -84,10 +84,10 @@ pub async fn get_provider_pipe_stream(cfg: &Config, pub async fn get_provider_reconnect_buffered_stream(cfg: &Config, http_client: &Arc, stream_url: &Url, - req: &HttpRequest, + req_headers: &HeaderMap, input: Option<&ConfigInput>, options: BufferStreamOptions) -> ProviderStreamResponse { - match create_provider_stream(cfg, Arc::clone(http_client), stream_url, req, input, options).await { + match create_provider_stream(cfg, Arc::clone(http_client), stream_url, req_headers, input, options).await { None => (None, None), Some((stream, info)) => { (Some(stream), info) diff --git a/src/api/model/streams/provider_stream_factory.rs b/src/api/model/streams/provider_stream_factory.rs index d731aa4d5..17665c30d 100644 --- a/src/api/model/streams/provider_stream_factory.rs +++ b/src/api/model/streams/provider_stream_factory.rs @@ -9,7 +9,6 @@ use crate::model::playlist::PlaylistItemType; use crate::tools::atomic_once_flag::AtomicOnceFlag; use crate::utils::debug_if_enabled; use crate::utils::network::request::{classify_content_type, get_request_headers, sanitize_sensitive_info, MimeCategory}; -use actix_web::HttpRequest; use bytes::Bytes; use futures::stream::{self, BoxStream}; use futures::{StreamExt, TryStreamExt}; @@ -153,7 +152,7 @@ impl ProviderStreamOptions { fn get_request_range_start_bytes(req_headers: &HashMap>) -> Option { // range header looks like bytes=1234-5566/2345345 or bytes=0- - if let Some(req_range) = req_headers.get(actix_web::http::header::RANGE.as_str()) { + if let Some(req_range) = req_headers.get(axum::http::header::RANGE.as_str()) { if let Some(bytes_range) = req_range.strip_prefix(b"bytes=") { if let Some(index) = bytes_range.iter().position(|&x| x == b'-') { let start_bytes = &bytes_range[..index]; @@ -169,13 +168,13 @@ fn get_request_range_start_bytes(req_headers: &HashMap>) -> Opti } fn get_client_stream_request_params( - req: &HttpRequest, + req_headers: &HeaderMap, input: Option<&ConfigInput>, options: &BufferStreamOptions) -> (usize, Option, bool, HeaderMap) { let stream_buffer_size = if options.is_buffer_enabled() { options.get_stream_buffer_size() } else { 1 }; let filter_header = get_header_filter_for_item_type(options.item_type); - let mut req_headers = get_headers_from_request(req, &filter_header); + let mut req_headers = get_headers_from_request(req_headers, &filter_header); debug_if_enabled!("Stream requested with headers: {:?}", req_headers.iter().map(|header| (header.0, String::from_utf8_lossy(header.1))).collect::>()); // we need the range bytes from client request for seek ing to the right position let req_range_start_bytes = get_request_range_start_bytes(&req_headers); @@ -277,7 +276,7 @@ async fn stream_provider(client: Arc, stream_options: ProviderS if !stream_options.should_continue() { return None; } - actix_web::rt::time::sleep(Duration::from_millis(100)).await; + tokio::time::sleep(Duration::from_millis(100)).await; } debug_if_enabled!("Stopped reconnecting stream {}", sanitize_sensitive_info(url.as_str())); None @@ -310,17 +309,17 @@ async fn get_initial_stream(cfg: &Config, client: Arc, stream_o break; } connect_err += 1; - actix_web::rt::time::sleep(Duration::from_millis(100)).await; + tokio::time::sleep(Duration::from_millis(100)).await; } stream_options.cancel_reconnect(); None } fn create_provider_stream_options(stream_url: &Url, - req: &HttpRequest, + req_headers: &HeaderMap, input: Option<&ConfigInput>, options: &BufferStreamOptions) -> ProviderStreamOptions { - let (buffer_size, req_range_start_bytes, reconnect, headers) = get_client_stream_request_params(req, input, options); + let (buffer_size, req_range_start_bytes, reconnect, headers) = get_client_stream_request_params(req_headers, input, options); let url = stream_url.clone(); let range_bytes = Arc::new(req_range_start_bytes.map(AtomicUsize::new)); let continue_flag = Arc::new(AtomicOnceFlag::new()); @@ -338,10 +337,10 @@ fn create_provider_stream_options(stream_url: &Url, pub async fn create_provider_stream(cfg: &Config, client: Arc, stream_url: &Url, - req: &HttpRequest, + req_headers: &HeaderMap, input: Option<&ConfigInput>, options: BufferStreamOptions) -> Option { - let stream_options = create_provider_stream_options(stream_url, req, input, &options); + let stream_options = create_provider_stream_options(stream_url, req_headers, input, &options); let client_stream_factory = |stream, reconnect_flag, range_cnt| { let stream = if stream_options.is_buffered() && !options.is_shared_stream() { @@ -407,7 +406,7 @@ pub async fn create_provider_stream(cfg: &Config, // let req = TestRequest::get().uri("/test").to_request(); // let _response = test::call_service(&server, req).await; // } -// async fn test_stream_handler(req: HttpRequest) -> HttpResponse { +// async fn test_stream_handler(req: axum::http::Request) -> impl axum::response::IntoResponse + Send { // let cfg = Config::default(); // let mut counter = 5; // let client = Arc::new(reqwest::Client::new()); diff --git a/src/api/model/streams/shared_stream_manager.rs b/src/api/model/streams/shared_stream_manager.rs index a0025c8d4..a2ee43cb1 100644 --- a/src/api/model/streams/shared_stream_manager.rs +++ b/src/api/model/streams/shared_stream_manager.rs @@ -3,12 +3,12 @@ use crate::api::model::streams::provider_stream_factory::STREAM_QUEUE_SIZE; use crate::api::model::stream_error::StreamError; use crate::utils::debug_if_enabled; use crate::utils::network::request::sanitize_sensitive_info; -use parking_lot::{RwLock}; use bytes::Bytes; use futures::stream::BoxStream; use futures::{Stream, StreamExt}; use std::collections::HashMap; -use std::sync::Arc; +use std::sync::{Arc}; +use tokio::sync::RwLock; use tokio::sync::mpsc::{Sender}; use std::pin::Pin; @@ -73,16 +73,16 @@ impl SharedStreamState { } } - fn subscribe(&self) -> BoxStream<'static, Result> { + async fn subscribe(&self) -> BoxStream<'static, Result> { let (tx, rx) = mpsc::channel(self.buf_size); - self.subscribers.write().push(tx); + self.subscribers.write().await.push(tx); convert_stream(ReceiverStream::new(rx).boxed()) } fn broadcast(&self, stream_url: &str, bytes_stream: S, shared_streams: Arc) where - S: Stream> + Unpin + 'static, - E: std::fmt::Debug + S: Stream> + Unpin + 'static + std::marker::Send, + E: std::fmt::Debug + std::marker::Send { let starving_size = self.buf_size-4; let sleep_duration = Duration::from_millis(10); @@ -92,28 +92,28 @@ impl SharedStreamState { let streaming_url = stream_url.to_string(); //Spawn a task to forward items from the source stream to the broadcast channel - actix_rt::spawn(async move { + tokio::spawn(async move { while let Some(item) = source_stream.next().await { if let Ok(data) = item { - if subscriber.read().is_empty() { + if subscriber.read().await.is_empty() { debug_if_enabled!("No active subscribers. Closing shared provider stream {}", sanitize_sensitive_info(&streaming_url)); // Cleanup for removing unused shared streams - shared_streams.unregister(&streaming_url); + shared_streams.unregister(&streaming_url).await; break; } let start_time = Instant::now(); loop { - if subscriber.read().iter().any(|sender| sender.capacity() >= starving_size) { + if subscriber.read().await.iter().any(|sender| sender.capacity() >= starving_size) { break; } - actix_web::rt::time::sleep(sleep_duration_starve).await; + tokio::time::sleep(sleep_duration_starve).await; if start_time.elapsed().as_secs() > 5 { break; } } - let mut subs = subscriber.write(); + let mut subs = subscriber.write().await; // TODO use drain_filter (*subs).retain(|sender| { match sender.try_send(data.clone()) { @@ -126,10 +126,10 @@ impl SharedStreamState { } }); } - actix_web::rt::time::sleep(sleep_duration).await; + tokio::time::sleep(sleep_duration).await; } debug_if_enabled!("Shared stream exhausted. Closing shared provider stream {}", sanitize_sensitive_info(&streaming_url)); - shared_streams.unregister(&streaming_url); + shared_streams.unregister(&streaming_url).await; }); } } @@ -147,46 +147,46 @@ impl SharedStreamManager { } } - pub fn get_shared_state_headers(&self, stream_url: &str) -> Option> { - self.shared_streams.read().get(stream_url).map(|s| s.headers.clone()) + pub async fn get_shared_state_headers(&self, stream_url: &str) -> Option> { + self.shared_streams.read().await.get(stream_url).map(|s| s.headers.clone()) } - fn unregister(&self, stream_url: &str) { - self.shared_streams.write().remove(stream_url); + async fn unregister(&self, stream_url: &str) { + let _ = self.shared_streams.write().await.remove(stream_url); } - fn subscribe_stream(&self, stream_url: &str) -> Option>> { - let stream_data = self.shared_streams.read().get(stream_url)?.subscribe(); + async fn subscribe_stream(&self, stream_url: &str) -> Option>> { + let stream_data = self.shared_streams.read().await.get(stream_url)?.subscribe().await; Some(stream_data) } - fn register(&self, stream_url: &str, shared_state: SharedStreamState) { - self.shared_streams.write().insert(stream_url.to_string(), shared_state); + async fn register(&self, stream_url: &str, shared_state: SharedStreamState) { + let _= self.shared_streams.write().await.insert(stream_url.to_string(), shared_state); } - pub(crate) fn subscribe( + pub(crate) async fn subscribe( app_state: &AppState, stream_url: &str, bytes_stream: S, headers: Vec<(String, String)>, buffer_size: usize,) where - S: Stream> + Unpin + 'static, - E: std::fmt::Debug + S: Stream> + Unpin + 'static + std::marker::Send, + E: std::fmt::Debug + std::marker::Send { let buf_size = std::cmp::max(buffer_size, STREAM_QUEUE_SIZE); let shared_state = SharedStreamState::new(headers, buf_size); shared_state.broadcast(stream_url, bytes_stream, Arc::clone(&app_state.shared_stream_manager)); - app_state.shared_stream_manager.register(stream_url, shared_state); + app_state.shared_stream_manager.register(stream_url, shared_state).await; debug_if_enabled!("Created shared provider stream {}", sanitize_sensitive_info(stream_url)); } /// Creates a broadcast notify stream for the given URL if a shared stream exists. - pub fn subscribe_shared_stream( + pub async fn subscribe_shared_stream( app_state: &AppState, stream_url: &str, ) -> Option>> { debug_if_enabled!("Responding existing shared client stream {}", sanitize_sensitive_info(stream_url)); - app_state.shared_stream_manager.subscribe_stream(stream_url) + app_state.shared_stream_manager.subscribe_stream(stream_url).await } } \ No newline at end of file diff --git a/src/api/model/xtream.rs b/src/api/model/xtream.rs index c22d172e2..1dd84fe59 100644 --- a/src/api/model/xtream.rs +++ b/src/api/model/xtream.rs @@ -1,8 +1,8 @@ use crate::model::api_proxy::{ApiProxyServerInfo, ProxyUserCredentials, ProxyUserStatus}; use chrono::{Duration, Local}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct XtreamUserInfoResponse { pub password: String, pub username: String, @@ -17,7 +17,7 @@ pub struct XtreamUserInfoResponse { pub status: String, // "Active" } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct XtreamServerInfoResponse { pub url: String, pub port: String, @@ -29,7 +29,7 @@ pub struct XtreamServerInfoResponse { pub time_now: String, //"2021-06-28 17:07:37" } -#[derive(Serialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct XtreamAuthorizationResponse { pub user_info: XtreamUserInfoResponse, pub server_info: XtreamServerInfoResponse, diff --git a/src/api/scheduler.rs b/src/api/scheduler.rs index df39f32c8..5dbd32ecf 100644 --- a/src/api/scheduler.rs +++ b/src/api/scheduler.rs @@ -31,7 +31,7 @@ pub async fn start_scheduler(client: Arc, expression: &str, con loop { let mut upcoming = schedule.upcoming(offset).take(1); if let Some(datetime) = upcoming.next() { - actix_web::rt::time::sleep_until(actix_rt::time::Instant::from(datetime_to_instant(datetime))).await; + tokio::time::sleep_until(tokio::time::Instant::from(datetime_to_instant(datetime))).await; exec_processing(Arc::clone(&client), Arc::clone(&config), Arc::clone(&targets)).await; } } @@ -63,7 +63,7 @@ mod tests { loop { let mut upcoming = schedule.upcoming(offset).take(1); if let Some(datetime) = upcoming.next() { - actix_web::rt::time::sleep_until(actix_rt::time::Instant::from(datetime_to_instant(datetime))).await; + tokio::time::sleep_until(actix_rt::time::Instant::from(datetime_to_instant(datetime))).await; run_me(); } if runs.load(Ordering::SeqCst) == 6 { diff --git a/src/auth/authenticator.rs b/src/auth/authenticator.rs index b62bd85a0..8083e4ed1 100644 --- a/src/auth/authenticator.rs +++ b/src/auth/authenticator.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use actix_web::{dev::ServiceRequest, Error, web}; -use actix_web_httpauth::extractors::bearer::BearerAuth; +use axum::debug_middleware; use chrono::{Local, Duration}; use jsonwebtoken::{Algorithm, DecodingKey, encode, decode, EncodingKey, Header, Validation, TokenData}; use crate::model::config::WebAuthConfig; @@ -46,12 +45,9 @@ fn create_jwt(web_auth_config: &WebAuthConfig, username: &str, roles: Vec, secret_key: &[u8]) -> Option> { - if let Some(auth) = bearer { - let token = auth.token(); - if let Ok(token_data) = decode::(token, &DecodingKey::from_secret(secret_key), &Validation::new(Algorithm::HS256)) { - return Some(token_data); - } +pub(crate) fn verify_token(token: &str, secret_key: &[u8]) -> Option> { + if let Ok(token_data) = decode::(token, &DecodingKey::from_secret(secret_key), &Validation::new(Algorithm::HS256)) { + return Some(token_data); } None } @@ -72,49 +68,51 @@ pub fn is_user(token_data: Option>) -> bool { has_role(token_data, ROLE_USER) } -pub fn verify_token_admin(bearer: Option, secret_key: &[u8]) -> bool { +pub fn verify_token_admin(bearer: &str, secret_key: &[u8]) -> bool { has_role(verify_token(bearer, secret_key), ROLE_ADMIN) } -pub fn verify_token_user(bearer: Option, secret_key: &[u8]) -> bool { +pub fn verify_token_user(bearer: &str, secret_key: &[u8]) -> bool { has_role(verify_token(bearer, secret_key), ROLE_USER) } fn validate_request( - req: ServiceRequest, - credentials: Option, - verify_fn: fn(Option, &[u8]) -> bool, // Funktions-Parameter für Admin/User-Check -) -> Result { - if let Some(app_state) = req.app_data::>>() { - if let Some(web_auth_config) = app_state.config.web_auth.as_ref() { - let secret_key = web_auth_config.secret.as_ref(); - if verify_fn(credentials, secret_key) { - return Ok(req); - } + app_state: &Arc, + token: &str, + verify_fn: fn(&str, &[u8]) -> bool, +) -> Result<(), ()> { + if let Some(web_auth_config) = app_state.config.web_auth.as_ref() { + let secret_key = web_auth_config.secret.as_ref(); + if verify_fn(token, secret_key) { + return Ok(()); } } - Err((actix_web::error::ErrorUnauthorized("Unauthorized"), req)) + Err(()) } +#[debug_middleware] pub async fn validator_admin( - req: ServiceRequest, - credentials: Option, -) -> Result { - validate_request(req, credentials, verify_token_admin) + axum::extract::State(app_state): axum::extract::State>, + axum_auth::AuthBearer(token): axum_auth::AuthBearer, + request: axum::extract::Request, + next: axum::middleware::Next, +) -> Result { + match validate_request(&app_state, &token, verify_token_admin) { + Ok(()) => Ok(next.run(request).await), + Err(()) => Err(axum::http::StatusCode::UNAUTHORIZED) + + } } +#[debug_middleware] pub async fn validator_user( - req: ServiceRequest, - credentials: Option, -) -> Result { - validate_request(req, credentials, verify_token_user) + axum::extract::State(app_state): axum::extract::State>, + axum_auth::AuthBearer(token): axum_auth::AuthBearer, + request: axum::extract::Request, + next: axum::middleware::Next, +) -> Result { + match validate_request(&app_state, &token, verify_token_user) { + Ok(()) => Ok(next.run(request).await), + Err(()) => Err(axum::http::StatusCode::UNAUTHORIZED) + } } - -// pub fn handle_unauthorized(srvres: ServiceResponse) -> actix_web::Result> { -// let (req, _) = srvres.into_parts(); -// let resp = HttpResponse::TemporaryRedirect().insert_header(("Location", "/auth/login")).finish(); -// let result = ServiceResponse::new(req, resp) -// .map_into_boxed_body() -// .map_into_right_body(); -// Ok(ErrorHandlerResponse::Response(result)) -// } diff --git a/src/foundation/filter.rs b/src/foundation/filter.rs index bdd5b849e..3fc7128f9 100644 --- a/src/foundation/filter.rs +++ b/src/foundation/filter.rs @@ -1,9 +1,6 @@ #![allow(clippy::empty_docs)] -use std::cell::RefCell; use std::collections::HashMap; -use std::rc::Rc; - use enum_iterator::all; use log::{debug, error, log_enabled, trace, Level}; use pest::iterators::Pair; @@ -16,21 +13,21 @@ use crate::tools::directed_graph::DirectedGraph; use crate::m3u_filter_error::{create_m3u_filter_error_result, info_err}; use crate::utils::sys_utils::exit; -pub fn get_field_value(pli: &PlaylistItem, field: &ItemField) -> Rc { - let header = pli.header.borrow(); +pub fn get_field_value(pli: &PlaylistItem, field: &ItemField) -> String { + let header = &pli.header; let value = match field { - ItemField::Group => &header.group, - ItemField::Name => &header.name, - ItemField::Title => &header.title, - ItemField::Url => &header.url, - ItemField::Input => &header.input_name, - ItemField::Type => &Rc::new(header.item_type.to_string()), + ItemField::Group => header.group.to_string(), + ItemField::Name => header.name.to_string(), + ItemField::Title => header.title.to_string(), + ItemField::Url => header.url.to_string(), + ItemField::Input => header.input_name.to_string(), + ItemField::Type => header.item_type.to_string(), }; - Rc::clone(value) + value.to_string() } -pub fn set_field_value(pli: &PlaylistItem, field: &ItemField, value: Rc) { - let header = &mut pli.header.borrow_mut(); +pub fn set_field_value(pli: &mut PlaylistItem, field: &ItemField, value: String) { + let header = &mut pli.header; match field { ItemField::Group => header.group = value, ItemField::Name => header.name = value, @@ -42,13 +39,12 @@ pub fn set_field_value(pli: &PlaylistItem, field: &ItemField, value: Rc) } pub struct ValueProvider<'a> { - pub pli: RefCell<&'a PlaylistItem>, + pub pli: &'a PlaylistItem, } impl ValueProvider<'_> { - fn call(&self, field: &ItemField) -> Rc { - let pli = *self.pli.borrow(); - get_field_value(pli, field) + fn call(&self, field: &ItemField) -> String { + get_field_value(self.pli, field) } } @@ -552,9 +548,6 @@ pub fn apply_templates_to_pattern(pattern: &str, templates: &Vec PlaylistItem { PlaylistItem { - header: RefCell::new(PlaylistItemHeader { - name: Rc::new(name.to_string()), - group: Rc::new(group.to_string()), + header: PlaylistItemHeader { + name: name.to_string(), + group: group.to_string(), ..Default::default() - }), + }, } } @@ -625,7 +618,7 @@ mod tests { .iter() .filter(|&chan| { let provider = ValueProvider { - pli: RefCell::new(chan), + pli: chan, }; filter.filter(&provider, &mut processor) }) @@ -681,7 +674,7 @@ mod tests { .iter() .filter(|&chan| { let provider = ValueProvider { - pli: RefCell::new(chan), + pli: chan, }; filter.filter(&provider, &mut processor) }) diff --git a/src/main.rs b/src/main.rs index 0a99a4d4f..afa407d91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,7 +9,6 @@ mod modules; include_modules!(); -use actix_rt::System; use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -155,18 +154,21 @@ fn main() { info!("Freeze frame video loaded from {:?}", cfg.channel_unavailable_file.as_ref().map_or("?", |v| v.as_str())); } - if args.server { - match config_reader::read_api_proxy_config(args.api_proxy, &mut cfg) { - Ok(Some(api_proxy_file)) => { - info!("Api Proxy File: {api_proxy_file:?}"); - }, - Ok(None) => {} - Err(err) => exit!("{err}"), + let rt = tokio::runtime::Runtime::new().unwrap(); + let () = rt.block_on(async { + if args.server { + match config_reader::read_api_proxy_config(args.api_proxy, &mut cfg).await { + Ok(Some(api_proxy_file)) => { + info!("Api Proxy File: {api_proxy_file:?}"); + }, + Ok(None) => {} + Err(err) => exit!("{err}"), + } + start_in_server_mode(Arc::new(cfg), Arc::new(targets)).await; + } else { + start_in_cli_mode(Arc::new(cfg), Arc::new(targets)).await; } - start_in_server_mode(Arc::new(cfg), Arc::new(targets)); - } else { - start_in_cli_mode(Arc::new(cfg), Arc::new(targets)); - } + }); } fn create_directories(cfg: &Config, temp_path: &Path) { @@ -198,13 +200,13 @@ fn create_directories(cfg: &Config, temp_path: &Path) { } } -fn start_in_cli_mode(cfg: Arc, targets: Arc) { +async fn start_in_cli_mode(cfg: Arc, targets: Arc) { let client = Arc::new(reqwest::Client::new()); - System::new().block_on(async { playlist::exec_processing(client, cfg, targets).await }); + playlist::exec_processing(client, cfg, targets).await; } -fn start_in_server_mode(cfg: Arc, targets: Arc) { - if let Err(err) = api::main_api::start_server(cfg, targets) { +async fn start_in_server_mode(cfg: Arc, targets: Arc) { + if let Err(err) = api::main_api::start_server(cfg, targets).await { exit!("Can't start server: {err}"); }; } diff --git a/src/messaging.rs b/src/messaging.rs index 25577ebde..eb3ae7729 100644 --- a/src/messaging.rs +++ b/src/messaging.rs @@ -22,7 +22,7 @@ fn send_http_post_request(msg: &str, messaging: &MessagingConfig) { if let Some(rest) = &messaging.rest { let url = rest.url.clone(); let data = msg.to_owned(); - actix_rt::spawn(async move { + tokio::spawn(async move { let client = reqwest::Client::new(); match client .post(&url) @@ -59,7 +59,7 @@ fn send_pushover_message(msg: &str, messaging: &MessagingConfig) { .append_pair("message", msg) .finish(); - actix_rt::spawn(async move { + tokio::spawn(async move { let client = reqwest::Client::new(); match client .post(url) diff --git a/src/model/api_proxy.rs b/src/model/api_proxy.rs index eb7a44cde..fb8345f5a 100644 --- a/src/model/api_proxy.rs +++ b/src/model/api_proxy.rs @@ -172,7 +172,7 @@ impl ProxyUserCredentials { Ok(()) } - pub fn has_permissions(&self, app_state: &AppState) -> bool { + pub async fn has_permissions(&self, app_state: &AppState) -> bool { if app_state.config.user_access_control { if let Some(exp_date) = self.exp_date.as_ref() { let now = Local::now(); @@ -182,7 +182,7 @@ impl ProxyUserCredentials { } } if let Some(max_connections) = self.max_connections.as_ref() { - if *max_connections < app_state.get_active_connections_for_user(&self.username) { + if *max_connections < app_state.get_active_connections_for_user(&self.username).await { debug!("User access denied, too many connections: {}", self.username); return false; } diff --git a/src/model/config.rs b/src/model/config.rs index c03149992..ac7216fa4 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -1,6 +1,5 @@ #![allow(clippy::struct_excessive_bools)] use enum_iterator::Sequence; -use parking_lot::RwLock; use std::borrow::BorrowMut; use std::collections::{HashMap, HashSet}; use std::fmt::Display; @@ -8,7 +7,8 @@ use std::fs::File; use std::io::BufRead; use std::path::PathBuf; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc}; +use tokio::sync::RwLock; use crate::auth::user::UserCredential; use log::{debug, error, warn}; @@ -1197,14 +1197,14 @@ pub struct Config { } impl Config { - pub fn set_api_proxy(&mut self, api_proxy: Option) -> Result<(), M3uFilterError> { + pub async fn set_api_proxy(&mut self, api_proxy: Option) -> Result<(), M3uFilterError> { self.t_api_proxy = Arc::new(RwLock::new(api_proxy)); - self.check_target_user() + self.check_target_user().await } - fn check_username(&self, output_username: Option<&str>, target_name: &str) -> Result<(), M3uFilterError> { + async fn check_username(&self, output_username: Option<&str>, target_name: &str) -> Result<(), M3uFilterError> { if let Some(username) = output_username { - if let Some((_, config_target)) = self.get_target_for_username(username) { + if let Some((_, config_target)) = self.get_target_for_username(username).await { if config_target.name != target_name { return create_m3u_filter_error_result!(M3uFilterErrorKind::Info, "User:{username} does not belong to target: {}", target_name); } @@ -1214,7 +1214,7 @@ impl Config { Ok(()) } } - fn check_target_user(&mut self) -> Result<(), M3uFilterError> { + async fn check_target_user(&mut self) -> Result<(), M3uFilterError> { let check_homerun = self.hdhomerun.as_ref().is_some_and(|h| h.enabled); for source in &self.sources { for target in &source.targets { @@ -1222,12 +1222,12 @@ impl Config { match output { TargetOutput::Xtream(_) | TargetOutput::M3u(_) => {} TargetOutput::Strm(strm_output) => { - self.check_username(strm_output.username.as_deref(), &target.name)?; + self.check_username(strm_output.username.as_deref(), &target.name).await?; } TargetOutput::HdHomeRun(hdhomerun_output) => { if check_homerun { let hdhr_name = &hdhomerun_output.device; - self.check_username(Some(&hdhomerun_output.username), &target.name)?; + self.check_username(Some(&hdhomerun_output.username), &target.name).await?; if let Some(homerun) = &mut self.hdhomerun { for device in &mut homerun.devices { if &device.name == hdhr_name { @@ -1282,23 +1282,24 @@ impl Config { None } - pub fn get_target_for_username(&self, username: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { - if let Some(credentials) = self.get_user_credentials(username) { - return self.t_api_proxy.read().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(&credentials.username, &credentials.password))); + pub async fn get_target_for_username(&self, username: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { + if let Some(credentials) = self.get_user_credentials(username).await { + return self.t_api_proxy.read().await.as_ref() + .and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(&credentials.username, &credentials.password))); } None } - pub fn get_target_for_user(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { - self.t_api_proxy.read().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(username, password))) + pub async fn get_target_for_user(&self, username: &str, password: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { + self.t_api_proxy.read().await.as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name(username, password))) } - pub fn get_target_for_user_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { - self.t_api_proxy.read().as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name_by_token(token))) + pub async fn get_target_for_user_by_token(&self, token: &str) -> Option<(ProxyUserCredentials, &ConfigTarget)> { + self.t_api_proxy.read().await.as_ref().and_then(|api_proxy| self.intern_get_target_for_user(api_proxy.get_target_name_by_token(token))) } - pub fn get_user_credentials(&self, username: &str) -> Option { - self.t_api_proxy.read().as_ref().and_then(|api_proxy| api_proxy.get_user_credentials(username)) + pub async fn get_user_credentials(&self, username: &str) -> Option { + self.t_api_proxy.read().await.as_ref().and_then(|api_proxy| api_proxy.get_user_credentials(username)) } pub fn get_input_by_name(&self, input_name: &str) -> Option<&ConfigInput> { @@ -1526,8 +1527,8 @@ impl Config { /// # Panics /// /// Will panic if default server invalid - pub fn get_user_server_info(&self, user: &ProxyUserCredentials) -> ApiProxyServerInfo { - let server_info_list = self.t_api_proxy.read().as_ref().unwrap().server.clone(); + pub async fn get_user_server_info(&self, user: &ProxyUserCredentials) -> ApiProxyServerInfo { + let server_info_list = self.t_api_proxy.read().await.as_ref().unwrap().server.clone(); let server_info_name = user.server.as_ref().map_or("default", |server_name| server_name.as_str()); server_info_list.iter().find(|c| c.name.eq(server_info_name)).map_or_else(|| server_info_list.first().unwrap().clone(), Clone::clone) } diff --git a/src/model/healthcheck.rs b/src/model/healthcheck.rs index 0b5c13e32..e592ae481 100644 --- a/src/model/healthcheck.rs +++ b/src/model/healthcheck.rs @@ -1,4 +1,6 @@ -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Healthcheck { pub status: String, pub version: String, diff --git a/src/model/mapping.rs b/src/model/mapping.rs index 098745791..6e1fa5623 100644 --- a/src/model/mapping.rs +++ b/src/model/mapping.rs @@ -2,10 +2,8 @@ use enum_iterator::Sequence; use log::{debug, error, trace}; use regex::{Regex}; use std::borrow::Cow; -use std::cell::RefCell; use std::collections::HashMap; use std::fmt::Display; -use std::rc::Rc; use std::str::FromStr; use std::sync::atomic::AtomicU32; use std::sync::Arc; @@ -270,23 +268,23 @@ impl Mapper { } pub struct MappingValueProcessor<'a> { - pub pli: RefCell<&'a PlaylistItem>, + pub pli: &'a mut PlaylistItem, pub mapper: &'a Mapper, } impl MappingValueProcessor<'_> { - fn get_property(&self, key: &str) -> Option> { - self.pli.borrow().header.borrow().get_field(key) + fn get_property(&self, key: &str) -> Option { + self.pli.header.get_field(key) } - fn set_property(&self, key: &str, value: &str) { - if !self.pli.borrow().header.borrow_mut().set_field(key, value) { + fn set_property(&mut self, key: &str, value: &str) { + if !self.pli.header.set_field(key, value) { error!("Cant set unknown field {} to {}", key, value); } trace!("Property {} set to {}", key, value); } - fn apply_attributes(&self, captured_names: &HashMap<&str, &str>) { + fn apply_attributes(&mut self, captured_names: &HashMap<&str, &str>) { let mapper = self.mapper; let attr_re = &mapper.t_attre.as_ref().unwrap(); let attributes = &mapper.attributes; @@ -303,7 +301,7 @@ impl MappingValueProcessor<'_> { } } - fn apply_tags(&self, value: &str, captures: &HashMap<&str, &str>) -> Option { + fn apply_tags(&mut self, value: &str, captures: &HashMap<&str, &str>) -> Option { let mut new_value = String::from(value); let tag_captures = self.mapper.t_tagre.as_ref().unwrap().captures_iter(value) .filter(|caps| caps.len() > 1) @@ -342,7 +340,7 @@ impl MappingValueProcessor<'_> { Some(new_value) } - fn apply_suffix(&self, captures: &HashMap<&str, &str>) { + fn apply_suffix(&mut self, captures: &HashMap<&str, &str>) { let mapper = self.mapper; let suffix = &mapper.suffix; @@ -356,7 +354,7 @@ impl MappingValueProcessor<'_> { } } - fn apply_prefix(&self, captures: &HashMap<&str, &str>) { + fn apply_prefix(&mut self, captures: &HashMap<&str, &str>) { let mapper = self.mapper; let prefix = &mapper.prefix; for (key, value) in prefix { @@ -369,7 +367,7 @@ impl MappingValueProcessor<'_> { } } - fn apply_assignments(&self) { + fn apply_assignments(&mut self) { let mapper = self.mapper; let assignments = &mapper.assignments; for (key, value) in assignments { @@ -387,7 +385,7 @@ impl MappingValueProcessor<'_> { } } - fn apply_transform(&self) { + fn apply_transform(&mut self) { let mapper = self.mapper; match &mapper.transform { None => {} diff --git a/src/model/playlist.rs b/src/model/playlist.rs index 6f7f041eb..4ac1fd00d 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -1,8 +1,5 @@ -use std::cell::RefCell; use std::cmp::PartialEq; use std::fmt::{Display, Formatter}; -use std::rc::Rc; - use crate::model::api_proxy::ProxyUserCredentials; use crate::model::config::{ConfigInput, ConfigTargetOptions}; use crate::model::xmltv::TVGuide; @@ -15,11 +12,11 @@ use crate::utils::network::request::extract_extension_from_url; // https://de.wikipedia.org/wiki/M3U // https://siptv.eu/howto/playlist.html -pub trait PlaylistEntry { +pub trait PlaylistEntry: Send + Sync { fn get_virtual_id(&self) -> u32; fn get_provider_id(&self) -> Option; fn get_category_id(&self) -> Option; - fn get_provider_url(&self) -> Rc; + fn get_provider_url(&self) -> String; fn get_uuid(&self) -> UUIDType; } @@ -129,7 +126,7 @@ impl Display for PlaylistItemType { } pub trait FieldGetAccessor { - fn get_field(&self, field: &str) -> Option>; + fn get_field(&self, field: &str) -> Option; } pub trait FieldSetAccessor { fn set_field(&mut self, field: &str, value: &str) -> bool; @@ -139,35 +136,35 @@ pub type UUIDType = [u8; 32]; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct PlaylistItemHeader { - pub uuid: Rc, // calculated - pub id: Rc, // provider id + pub uuid: UUIDType, // calculated + pub id: String, // provider id pub virtual_id: u32, // virtual id - pub name: Rc, - pub chno: Rc, - pub logo: Rc, - pub logo_small: Rc, - pub group: Rc, - pub title: Rc, - pub parent_code: Rc, - pub audio_track: Rc, - pub time_shift: Rc, - pub rec: Rc, - pub url: Rc, - pub epg_channel_id: Option>, + pub name: String, + pub chno: String, + pub logo: String, + pub logo_small: String, + pub group: String, + pub title: String, + pub parent_code: String, + pub audio_track: String, + pub time_shift: String, + pub rec: String, + pub url: String, + pub epg_channel_id: Option, pub xtream_cluster: XtreamCluster, pub additional_properties: Option, #[serde(default, skip_serializing, skip_deserializing)] pub item_type: PlaylistItemType, #[serde(default)] pub category_id: u32, - pub input_name: Rc, + pub input_name: String, } impl PlaylistItemHeader { pub fn gen_uuid(&mut self) { - self.uuid = Rc::new(generate_playlist_uuid(&self.input_name, &self.id, self.item_type, &self.url)); + self.uuid = generate_playlist_uuid(&self.input_name, &self.id, self.item_type, &self.url); } - pub const fn get_uuid(&self) -> &Rc { + pub const fn get_uuid(&self) -> &UUIDType { &self.uuid } @@ -175,7 +172,7 @@ impl PlaylistItemHeader { match get_provider_id(&self.id, &self.url) { None => None, Some(newid) => { - self.id = Rc::new(newid.to_string()); + self.id = newid.to_string(); Some(newid) } } @@ -233,7 +230,7 @@ macro_rules! to_m3u_resource_non_empty_fields { macro_rules! generate_field_accessor_impl_for_playlist_item_header { ($($prop:ident),*;) => { impl FieldGetAccessor for PlaylistItemHeader { - fn get_field(&self, field: &str) -> Option> { + fn get_field(&self, field: &str) -> Option { match field { $( stringify!($prop) => Some(self.$prop.clone()), @@ -249,12 +246,12 @@ macro_rules! generate_field_accessor_impl_for_playlist_item_header { match field { $( stringify!($prop) => { - self.$prop = Rc::new(val); + self.$prop = val; true } )* "epg_channel_id" | "epg_id" => { - self.epg_channel_id = Some(Rc::new(value.to_owned())); + self.epg_channel_id = Some(value.to_owned()); true } _ => false, @@ -269,20 +266,20 @@ generate_field_accessor_impl_for_playlist_item_header!(id, /*virtual_id,*/ name, #[derive(Debug, Clone, Serialize, Deserialize)] pub struct M3uPlaylistItem { pub virtual_id: u32, - pub provider_id: Rc, - pub name: Rc, - pub chno: Rc, - pub logo: Rc, - pub logo_small: Rc, - pub group: Rc, - pub title: Rc, - pub parent_code: Rc, - pub audio_track: Rc, - pub time_shift: Rc, - pub rec: Rc, - pub url: Rc, - pub epg_channel_id: Option>, - pub input_name: Rc, + pub provider_id: String, + pub name: String, + pub chno: String, + pub logo: String, + pub logo_small: String, + pub group: String, + pub title: String, + pub parent_code: String, + pub audio_track: String, + pub time_shift: String, + pub rec: String, + pub url: String, + pub epg_channel_id: Option, + pub input_name: String, pub item_type: PlaylistItemType, #[serde(skip)] pub t_stream_url: String, @@ -333,8 +330,8 @@ impl PlaylistEntry for M3uPlaylistItem { None } #[inline] - fn get_provider_url(&self) -> Rc { - Rc::clone(&self.url) + fn get_provider_url(&self) -> String { + self.url.to_string() } fn get_uuid(&self) -> UUIDType { @@ -345,7 +342,7 @@ impl PlaylistEntry for M3uPlaylistItem { macro_rules! generate_field_accessor_impl_for_m3u_playlist_item { ($($prop:ident),*;) => { impl FieldGetAccessor for M3uPlaylistItem { - fn get_field(&self, field: &str) -> Option> { + fn get_field(&self, field: &str) -> Option { match field { $( stringify!($prop) => Some(self.$prop.clone()), @@ -364,20 +361,20 @@ generate_field_accessor_impl_for_m3u_playlist_item!(provider_id, name, chno, log pub struct XtreamPlaylistItem { pub virtual_id: u32, pub provider_id: u32, - pub name: Rc, - pub logo: Rc, - pub logo_small: Rc, - pub group: Rc, - pub title: Rc, - pub parent_code: Rc, - pub rec: Rc, - pub url: Rc, - pub epg_channel_id: Option>, + pub name: String, + pub logo: String, + pub logo_small: String, + pub group: String, + pub title: String, + pub parent_code: String, + pub rec: String, + pub url: String, + pub epg_channel_id: Option, pub xtream_cluster: XtreamCluster, pub additional_properties: Option, pub item_type: PlaylistItemType, pub category_id: u32, - pub input_name: Rc, + pub input_name: String, pub channel_no: u32, } @@ -410,8 +407,8 @@ impl PlaylistEntry for XtreamPlaylistItem { None } #[inline] - fn get_provider_url(&self) -> Rc { - Rc::clone(&self.url) + fn get_provider_url(&self) -> String { + self.url.to_string() } #[inline] @@ -420,22 +417,22 @@ impl PlaylistEntry for XtreamPlaylistItem { } } -pub fn get_backdrop_path_value(field: &str, value: Option<&Value>) -> Option> { +pub fn get_backdrop_path_value(field: &str, value: Option<&Value>) -> Option { match value { - Some(Value::String(url)) => Some(Rc::new(url.clone())), + Some(Value::String(url)) => Some(url.clone()), Some(Value::Array(values)) => { match values.as_slice() { - [Value::String(single)] => Some(Rc::new(single.to_string())), + [Value::String(single)] => Some(single.to_string()), multiple if !multiple.is_empty() => { if let Some(index) = field.rfind('_') { if let Ok(bd_index) = field[index + 1..].parse::() { if let Some(Value::String(selected)) = multiple.get(bd_index) { - return Some(Rc::new(selected.to_string())); + return Some(selected.to_string()); } } } if let Value::String(url) = &multiple[0] { - Some(Rc::new(url.to_string())) + Some(url.to_string()) } else { None } @@ -450,7 +447,7 @@ pub fn get_backdrop_path_value(field: &str, value: Option<&Value>) -> Option { impl FieldGetAccessor for XtreamPlaylistItem { - fn get_field(&self, field: &str) -> Option> { + fn get_field(&self, field: &str) -> Option { match field { $( stringify!($prop) => Some(self.$prop.clone()), @@ -462,7 +459,7 @@ macro_rules! generate_field_accessor_impl_for_xtream_playlist_item { return match props { Some(doc) => { return if field == PROP_COVER { - doc.get(field).and_then(|value| value.as_str().map(|s| Rc::new(s.to_string()))) + doc.get(field).and_then(|value| value.as_str().map(|s| s.to_string())) } else { get_backdrop_path_value(field, doc.get(PROP_BACKDROP_PATH)) } @@ -480,30 +477,30 @@ macro_rules! generate_field_accessor_impl_for_xtream_playlist_item { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlaylistItem { - pub header: RefCell, + pub header: PlaylistItemHeader, } generate_field_accessor_impl_for_xtream_playlist_item!(name, logo, logo_small, group, title, parent_code, rec, url;); impl PlaylistItem { pub fn to_m3u(&self) -> M3uPlaylistItem { - let header = self.header.borrow(); + let header = &self.header; M3uPlaylistItem { virtual_id: header.virtual_id, - provider_id: Rc::clone(&header.id), - name: Rc::clone(if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }), - chno: Rc::clone(&header.chno), - logo: Rc::clone(&header.logo), - logo_small: Rc::clone(&header.logo_small), - group: Rc::clone(&header.group), - title: Rc::clone(&header.title), - parent_code: Rc::clone(&header.parent_code), - audio_track: Rc::clone(&header.audio_track), - time_shift: Rc::clone(&header.time_shift), - rec: Rc::clone(&header.rec), - url: Rc::clone(&header.url), + provider_id: header.id.to_string(), + name: if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }.to_string(), + chno: header.chno.to_string(), + logo: header.logo.to_string(), + logo_small: header.logo_small.to_string(), + group: header.group.to_string(), + title: header.title.to_string(), + parent_code: header.parent_code.to_string(), + audio_track: header.audio_track.to_string(), + time_shift: header.time_shift.to_string(), + rec: header.rec.to_string(), + url: header.url.to_string(), epg_channel_id: header.epg_channel_id.clone(), - input_name: Rc::clone(&header.input_name), + input_name: header.input_name.to_string(), item_type: header.item_type, t_stream_url: header.url.to_string(), t_resource_url: None, @@ -511,7 +508,7 @@ impl PlaylistItem { } pub fn to_xtream(&self) -> XtreamPlaylistItem { - let header = self.header.borrow(); + let header = &self.header; let provider_id = header.id.parse::().unwrap_or_default(); let mut additional_properties = None; if header.xtream_cluster != XtreamCluster::Live { @@ -549,20 +546,20 @@ impl PlaylistItem { XtreamPlaylistItem { virtual_id: header.virtual_id, provider_id, - name: Rc::clone(if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }), - logo: Rc::clone(&header.logo), - logo_small: Rc::clone(&header.logo_small), - group: Rc::clone(&header.group), - title: Rc::clone(&header.title), - parent_code: Rc::clone(&header.parent_code), - rec: Rc::clone(&header.rec), - url: Rc::clone(&header.url), + name: if header.item_type == PlaylistItemType::Series { &header.title } else { &header.name }.to_string(), + logo: header.logo.to_string(), + logo_small: header.logo_small.to_string(), + group: header.group.to_string(), + title: header.title.to_string(), + parent_code: header.parent_code.to_string(), + rec: header.rec.to_string(), + url: header.url.to_string(), epg_channel_id: header.epg_channel_id.clone(), xtream_cluster: header.xtream_cluster, additional_properties, item_type: header.item_type, category_id: header.category_id, - input_name: Rc::clone(&header.input_name), + input_name: header.input_name.to_string(), channel_no: header.chno.parse::().unwrap_or(0) } } @@ -571,11 +568,11 @@ impl PlaylistItem { impl PlaylistEntry for PlaylistItem { #[inline] fn get_virtual_id(&self) -> u32 { - self.header.borrow().virtual_id + self.header.virtual_id } fn get_provider_id(&self) -> Option { - let header = self.header.borrow(); + let header = &self.header; get_provider_id(&header.id, &header.url) } @@ -585,12 +582,12 @@ impl PlaylistEntry for PlaylistItem { } #[inline] - fn get_provider_url(&self) -> Rc { - Rc::clone(&self.header.borrow().url) + fn get_provider_url(&self) -> String { + self.header.url.to_string() } #[inline] fn get_uuid(&self) -> UUIDType { - let header = self.header.borrow(); + let header = &self.header; generate_playlist_uuid(&header.input_name, &header.id, header.item_type, &header.url) } } @@ -598,7 +595,7 @@ impl PlaylistEntry for PlaylistItem { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PlaylistGroup { pub id: u32, - pub title: Rc, + pub title: String, pub channels: Vec, #[serde(skip_serializing, skip_deserializing)] pub xtream_cluster: XtreamCluster, @@ -607,7 +604,9 @@ pub struct PlaylistGroup { impl PlaylistGroup { #[inline] pub fn on_load(&mut self) { - self.channels.iter().for_each(|pl| pl.header.borrow_mut().gen_uuid()); + for pl in &mut self.channels { + pl.header.gen_uuid(); + } } #[inline] diff --git a/src/model/xmltv.rs b/src/model/xmltv.rs index 695dd9663..78fffe10f 100644 --- a/src/model/xmltv.rs +++ b/src/model/xmltv.rs @@ -1,7 +1,6 @@ use std::collections::{HashMap}; use std::path::PathBuf; -use std::rc::Rc; - +use std::sync::Arc; use quick_xml::{Error, Writer}; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; @@ -17,8 +16,8 @@ pub const EPG_ATTRIB_CHANNEL: &str = "channel"; pub struct XmlTag { pub name: String, pub value: Option, - pub attributes: Option>>, - pub children: Option>>, + pub attributes: Option>>, + pub children: Option>>, } impl XmlTag { @@ -45,7 +44,7 @@ impl XmlTag { #[derive(Debug, Clone)] pub struct Epg { - pub attributes: Option>>, + pub attributes: Option>>, pub children: Vec, } diff --git a/src/model/xtream.rs b/src/model/xtream.rs index 49005a2c8..3ec2ad0df 100644 --- a/src/model/xtream.rs +++ b/src/model/xtream.rs @@ -1,7 +1,5 @@ use std::collections::HashMap; use std::iter::FromIterator; -use std::rc::Rc; - use crate::model::api_proxy::{ProxyType, ProxyUserCredentials}; use crate::model::config::{Config, XtreamTargetOutput}; use crate::model::playlist::{PlaylistEntry, PlaylistItem, XtreamCluster, XtreamPlaylistItem}; @@ -79,29 +77,29 @@ fn value_to_string(v: &Value) -> Option { } } -fn deserialize_as_option_rc_string<'de, D>(deserializer: D) -> Result>, D::Error> +fn deserialize_as_option_rc_string<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let value: Value = Deserialize::deserialize(deserializer)?; match &value { - Value::String(s) => Ok(Some(Rc::new(s.to_owned()))), - Value::Number(s) => Ok(Some(Rc::new(s.to_string()))), + Value::String(s) => Ok(Some(s.to_owned())), + Value::Number(s) => Ok(Some(s.to_string())), _ => Ok(None), } } -fn deserialize_as_rc_string<'de, D>(deserializer: D) -> Result, D::Error> +fn deserialize_as_rc_string<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let value: Value = Deserialize::deserialize(deserializer)?; match &value { - Value::String(s) => Ok(Rc::new(s.to_owned())), - Value::Null => Ok(Rc::new(String::new())), - _ => Ok(Rc::new(value.to_string())), + Value::String(s) => Ok(s.to_string()), + Value::Null => Ok(String::new()), + _ => Ok(value.to_string()), } } @@ -119,9 +117,9 @@ where #[derive(Deserialize, Default)] pub struct XtreamCategory { #[serde(deserialize_with = "deserialize_as_rc_string")] - pub category_id: Rc, + pub category_id: String, #[serde(deserialize_with = "deserialize_as_rc_string")] - pub category_name: Rc, + pub category_name: String, //pub parent_id: i32, #[serde(default)] pub channels: Vec, @@ -136,65 +134,65 @@ impl XtreamCategory { #[derive(Serialize, Deserialize)] pub struct XtreamStream { #[serde(default, deserialize_with = "deserialize_as_rc_string")] - pub name: Rc, + pub name: String, #[serde(default, deserialize_with = "deserialize_as_rc_string")] - pub category_id: Rc, + pub category_id: String, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub stream_id: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub series_id: Option, #[serde(default, deserialize_with = "deserialize_as_rc_string")] - pub stream_icon: Rc, + pub stream_icon: String, #[serde(default, deserialize_with = "deserialize_as_rc_string")] - pub direct_source: Rc, + pub direct_source: String, // optional attributes #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub custom_sid: Option>, + pub custom_sid: Option, #[serde(default, deserialize_with = "deserialize_as_string_array")] pub backdrop_path: Option>, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub added: Option>, + pub added: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub cast: Option>, + pub cast: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub container_extension: Option>, + pub container_extension: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub cover: Option>, + pub cover: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub director: Option>, + pub director: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub episode_run_time: Option>, + pub episode_run_time: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub genre: Option>, + pub genre: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub last_modified: Option>, + pub last_modified: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub plot: Option>, + pub plot: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub rating: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub rating_5based: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub release_date: Option>, + pub release_date: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub stream_type: Option>, + pub stream_type: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub title: Option>, + pub title: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub year: Option>, + pub year: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub trailer: Option>, + pub trailer: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub youtube_trailer: Option>, + pub youtube_trailer: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub epg_channel_id: Option>, + pub epg_channel_id: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub tv_archive: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub tv_archive_duration: Option, #[serde(default, deserialize_with = "deserialize_as_option_rc_string")] - pub tmdb: Option>, + pub tmdb: Option, #[serde(default, deserialize_with = "deserialize_number_from_string")] pub is_adult: Option, @@ -561,19 +559,19 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, opti let logo_small_url = if pli.logo_small.is_empty() { String::new() } else { format!("{resource_url}/logo_small") }; (Some(resource_url), logo_url, logo_small_url) } else { - (None, pli.logo.as_ref().clone(), pli.logo_small.as_ref().clone()) + (None, pli.logo.clone(), pli.logo_small.clone()) } } ProxyType::Redirect => { - (None, pli.logo.as_ref().clone(), pli.logo_small.as_ref().clone()) + (None, pli.logo.clone(), pli.logo_small.clone()) } }; let mut document = serde_json::Map::from_iter([ ("category_id".to_string(), Value::String(format!("{}", &pli.category_id))), ("category_ids".to_string(), Value::Array(Vec::from([Value::Number(serde_json::Number::from(pli.category_id))]))), - ("name".to_string(), Value::String(pli.name.as_ref().clone())), + ("name".to_string(), Value::String(pli.name.clone())), ("num".to_string(), Value::Number(serde_json::Number::from(pli.channel_no))), - ("title".to_string(), Value::String(pli.title.as_ref().clone())), + ("title".to_string(), Value::String(pli.title.clone())), ("stream_icon".to_string(), Value::String(logo)), ]); @@ -583,18 +581,18 @@ pub fn xtream_playlistitem_to_document(pli: &XtreamPlaylistItem, url: &str, opti if options.skip_live_direct_source { document.insert("direct_source".to_string(), Value::String(String::new())); } else { - document.insert("direct_source".to_string(), Value::String(pli.url.as_ref().clone())); + document.insert("direct_source".to_string(), Value::String(pli.url.clone())); } document.insert("thumbnail".to_string(), Value::String(logo_small)); document.insert("custom_sid".to_string(), Value::String(String::new())); - document.insert("epg_channel_id".to_string(), pli.epg_channel_id.as_ref().map_or(Value::Null, |epg_id| Value::String(epg_id.as_ref().clone()))); + document.insert("epg_channel_id".to_string(), pli.epg_channel_id.as_ref().map_or(Value::Null, |epg_id| Value::String(epg_id.clone()))); } XtreamCluster::Video => { document.insert("stream_id".to_string(), stream_id_value); if options.skip_video_direct_source { document.insert("direct_source".to_string(), Value::String(String::new())); } else { - document.insert("direct_source".to_string(), Value::String(pli.url.as_ref().clone())); + document.insert("direct_source".to_string(), Value::String(pli.url.clone())); } document.insert("custom_sid".to_string(), Value::String(String::new())); } diff --git a/src/processing/parser/m3u.rs b/src/processing/parser/m3u.rs index 4307f17cd..1d87c0aab 100644 --- a/src/processing/parser/m3u.rs +++ b/src/processing/parser/m3u.rs @@ -1,7 +1,4 @@ use std::borrow::BorrowMut; -use std::cell::RefCell; -use std::rc::Rc; - use crate::model::config::{Config, ConfigInput}; use crate::model::playlist::{PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, XtreamCluster}; use crate::utils::hash_utils::extract_id_from_url; @@ -76,9 +73,9 @@ fn skip_digit(it: &mut std::str::Chars) -> Option { fn create_empty_playlistitem_header(input_name: &str, url: &str) -> PlaylistItemHeader { PlaylistItemHeader { - url: Rc::new(url.to_owned()), + url: url.to_owned(), category_id: 0, - input_name: Rc::new(input_name.to_string()), + input_name: input_name.to_string(), ..Default::default() } } @@ -87,7 +84,7 @@ macro_rules! process_header_fields { ($header:expr, $token:expr, $(($prop:ident, $field:expr)),*; $val:expr) => { match $token { $( - $field => $header.$prop = Rc::new($val), + $field => $header.$prop = $val, )* _ => {} } @@ -105,7 +102,7 @@ fn process_header(input: &ConfigInput, video_suffixes: &[&str], content: &str, u break; } if c.unwrap() == ',' { - plih.title = Rc::new(get_value(&mut it)); + plih.title = get_value(&mut it); } else { let token = token_till(&mut it, '=', true); if let Some(t) = token { @@ -127,11 +124,11 @@ fn process_header(input: &ConfigInput, video_suffixes: &[&str], content: &str, u } if plih.id.is_empty() { if let Some(chanid) = extract_id_from_url(url) { - plih.id = Rc::new(chanid); + plih.id = chanid; } } // plih.virtual_id = plih.id; - plih.epg_channel_id = Some(Rc::clone(&plih.id)); + plih.epg_channel_id = Some(plih.id.to_string()); } if video_suffixes.iter().any(|suffix| url.ends_with(suffix)) { @@ -177,17 +174,16 @@ where continue; } if let Some(header_value) = header { - let item = PlaylistItem { header: RefCell::new(process_header(input, &video_suffixes, &header_value, line)) }; - let mut header = item.header.borrow_mut(); + let mut item = PlaylistItem { header: process_header(input, &video_suffixes, &header_value, line) }; + let header = &mut item.header; if header.group.is_empty() { if let Some(group_value) = group { - header.group = Rc::new(group_value); + header.group = group_value; } else { let current_title = header.title.clone(); - header.group = Rc::new(string_utils::get_title_group(current_title.as_str())); + header.group = string_utils::get_title_group(current_title.as_str()); } } - drop(header); visit(item); } header = None; @@ -205,7 +201,7 @@ where consume_m3u(cfg, input, lines, |item| { // keep the original sort order for groups and group the playlist items let key = { - let header = item.header.borrow(); + let header = &item.header; format!("{}{}", &header.xtream_cluster, &header.group) }; match group_map.entry(key) { @@ -224,9 +220,9 @@ where // create a group based on the first playlist item let channel = channels.first(); let (cluster, group_title) = channel.map(|pli| - (pli.header.borrow().xtream_cluster, Rc::clone(&pli.header.borrow().group))).unwrap(); + (pli.header.xtream_cluster, &pli.header.group)).unwrap(); grp_id += 1; - PlaylistGroup { id: grp_id, xtream_cluster: cluster, title: Rc::clone(&group_title), channels } + PlaylistGroup { id: grp_id, xtream_cluster: cluster, title: group_title.to_string(), channels } }).collect(); result } diff --git a/src/processing/parser/xmltv.rs b/src/processing/parser/xmltv.rs index 9787cff43..8eb76a568 100644 --- a/src/processing/parser/xmltv.rs +++ b/src/processing/parser/xmltv.rs @@ -1,6 +1,5 @@ use std::collections::{HashMap, HashSet}; -use std::rc::Rc; - +use std::sync::Arc; use quick_xml::events::Event; use quick_xml::Reader; @@ -8,14 +7,14 @@ use crate::model::xmltv::{Epg, EPG_ATTRIB_CHANNEL, EPG_ATTRIB_ID, EPG_TAG_TV, EP use crate::utils::compression::compressed_file_reader::CompressedFileReader; impl TVGuide { - pub fn filter(&self, channel_ids: &HashSet>) -> Option { + pub fn filter(&self, channel_ids: &HashSet) -> Option { if channel_ids.is_empty() { return None; } match CompressedFileReader::new(&self.file) { Ok(mut reader) => { let mut children: Vec = vec![]; - let mut tv_attributes: Option>> = None; + let mut tv_attributes: Option>> = None; let mut filter_tags = |tag: XmlTag| { if match tag.name.as_str() { EPG_TAG_CHANNEL => { @@ -76,7 +75,7 @@ where let tag = XmlTag { name, value: None, - attributes: if attributes.is_empty() { None } else { Some(Rc::new(attributes)) }, + attributes: if attributes.is_empty() { None } else { Some(Arc::new(attributes)) }, children: None, }; @@ -103,7 +102,7 @@ where } } else if !stack.is_empty() { if let Some(old_tag) = stack.pop().map(|mut r| { - let rc_tag = Rc::new(tag); + let rc_tag = Arc::new(tag); r.children = Some( r.children.map_or_else(|| vec![rc_tag.clone()], |mut c| { c.push(rc_tag.clone()); @@ -182,7 +181,7 @@ mod tests { let tv_guide = TVGuide { file: PathBuf::from(file_path) }; let channel_ids = vec!["channel.1", "channel.2", "channel.3"]; - let channel_ids : HashSet> = channel_ids.into_iter().map(|s| Rc::new(s.to_string())).collect(); + let channel_ids : HashSet> = channel_ids.into_iter().map(|s| Arc::new(s.to_string())).collect(); match tv_guide.filter(&channel_ids) { None => assert!(false, "No epg filtered"), diff --git a/src/processing/parser/xtream.rs b/src/processing/parser/xtream.rs index 7db65e666..cbe79bebf 100644 --- a/src/processing/parser/xtream.rs +++ b/src/processing/parser/xtream.rs @@ -1,7 +1,5 @@ -use std::cell::RefCell; use std::collections::HashMap; -use std::rc::Rc; - +use std::sync::Arc; use serde_json::Value; use crate::m3u_filter_error::{M3uFilterError, M3uFilterErrorKind, create_m3u_filter_error_result}; @@ -29,13 +27,13 @@ fn map_to_xtream_streams(xtream_cluster: XtreamCluster, streams: &Value) -> Resu } } -fn create_xtream_series_episode_url(url: &str, username: &str, password: &str, episode: &XtreamSeriesInfoEpisode) -> Rc { +fn create_xtream_series_episode_url(url: &str, username: &str, password: &str, episode: &XtreamSeriesInfoEpisode) -> Arc { if episode.direct_source.is_empty() { let ext = episode.container_extension.clone(); let stream_base_url = format!("{url}/series/{username}/{password}/{}.{ext}", episode.id); - Rc::new(stream_base_url) + Arc::new(stream_base_url) } else { - Rc::new(episode.direct_source.clone()) + Arc::new(episode.direct_source.clone()) } } @@ -51,21 +49,21 @@ pub fn parse_xtream_series_info(info: &Value, group_title: &str, series_name: &s let episode_url = create_xtream_series_episode_url(url, username, password, episode); (episode.clone(), PlaylistItem { - header: RefCell::new(PlaylistItemHeader { - id: Rc::new(episode.id.to_string()), - uuid: Rc::new(generate_playlist_uuid(&input.name, &episode.id, PlaylistItemType::Series, &episode_url)), - name: Rc::new(series_name.to_string()), - logo: Rc::new(episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string())), - group: Rc::new(group_title.to_string()), - title: Rc::new(episode.title.clone()), - url: episode_url, + header: PlaylistItemHeader { + id: episode.id.to_string(), + uuid: generate_playlist_uuid(&input.name, &episode.id, PlaylistItemType::Series, &episode_url), + name: series_name.to_string(), + logo: episode.info.as_ref().map_or_else(String::new, |info| info.movie_image.to_string()), + group: group_title.to_string(), + title: episode.title.clone(), + url: episode_url.to_string(), item_type: PlaylistItemType::Series, xtream_cluster: XtreamCluster::Series, additional_properties: episode.get_additional_properties(&series_info), category_id: 0, - input_name: Rc::new(input.name.to_string()), + input_name: input.name.to_string(), ..Default::default() - }) + } }) }).collect(); return if result.is_empty() { Ok(None) } else { Ok(Some(result)) }; @@ -100,13 +98,13 @@ pub fn get_xtream_url(xtream_cluster: XtreamCluster, url: &str, } pub fn create_xtream_url(xtream_cluster: XtreamCluster, url: &str, username: &str, password: &str, - stream: &XtreamStream, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> Rc { + stream: &XtreamStream, live_stream_use_prefix: bool, live_stream_without_extension: bool) -> String { if stream.direct_source.is_empty() { - Rc::new(get_xtream_url(xtream_cluster, url, username, password, stream.get_stream_id(), + get_xtream_url(xtream_cluster, url, username, password, stream.get_stream_id(), stream.container_extension.as_ref().map(std::string::ToString::to_string).as_ref(), - live_stream_use_prefix, live_stream_without_extension)) + live_stream_use_prefix, live_stream_without_extension) } else { - Rc::clone(&stream.direct_source) + stream.direct_source.to_string() } } @@ -116,65 +114,63 @@ pub fn parse_xtream(input: &ConfigInput, streams: &Value) -> Result>, M3uFilterError> { match map_to_xtream_category(categories) { Ok(xtream_categories) => { - let input_name = Rc::new(input.name.to_string()); + let input_name = Arc::new(input.name.to_string()); let url = input.url.as_str(); let username = input.username.as_ref().map_or("", |v| v); let password = input.password.as_ref().map_or("", |v| v); match map_to_xtream_streams(xtream_cluster, streams) { - Ok(xtream_streams) => { - let mut group_map: HashMap::, RefCell> = + Ok(mut xtream_streams) => { + let mut group_map: HashMap:: = xtream_categories.into_iter().map(|category| - (Rc::clone(&category.category_id), RefCell::new(category)) + (category.category_id.to_string(), category) ).collect(); - let unknown_grp = RefCell::new(XtreamCategory { - category_id: Rc::new("0".to_string()), - category_name: Rc::new("Unknown".to_string()), + let mut unknown_grp = XtreamCategory { + category_id: "0".to_string(), + category_name: "Unknown".to_string(), channels: vec![], - }); + }; let (live_stream_use_prefix, live_stream_without_extension) = input.options.as_ref() .map_or((true, false), |o| (o.xtream_live_stream_use_prefix, o.xtream_live_stream_without_extension)); - for stream in xtream_streams { - let group = group_map.get(&stream.category_id).unwrap_or(&unknown_grp); - let mut grp = group.borrow_mut(); - let category_name = &grp.category_name; - let stream_url = create_xtream_url(xtream_cluster, url, username, password, &stream, live_stream_use_prefix, live_stream_without_extension); + for stream in &mut xtream_streams { + let group = group_map.get_mut(&stream.category_id).unwrap_or(&mut unknown_grp); + let category_name = &group.category_name; + let stream_url = create_xtream_url(xtream_cluster, url, username, password, stream, live_stream_use_prefix, live_stream_without_extension); let item_type = PlaylistItemType::from(xtream_cluster); let item = PlaylistItem { - header: RefCell::new(PlaylistItemHeader { - id: Rc::new(stream.get_stream_id().to_string()), - uuid: Rc::new(generate_playlist_uuid(&input_name, &stream.get_stream_id().to_string(), item_type, &stream_url)), - name: Rc::clone(&stream.name), - logo: Rc::clone(&stream.stream_icon), - group: Rc::clone(category_name), - title: Rc::clone(&stream.name), - url: stream_url, + header: PlaylistItemHeader { + id: stream.get_stream_id().to_string(), + uuid: generate_playlist_uuid(&input_name, &stream.get_stream_id().to_string(), item_type, &stream_url), + name: stream.name.to_string(), + logo: stream.stream_icon.to_string(), + group: category_name.to_string(), + title: stream.name.to_string(), + url: stream_url.to_string(), epg_channel_id: stream.epg_channel_id.clone(), item_type, xtream_cluster, additional_properties: stream.get_additional_properties(), category_id: 0, - input_name: Rc::clone(&input_name), + input_name: input_name.to_string(), ..Default::default() - }), + }, }; - grp.add(item); + group.add(item); } - let has_channels = !unknown_grp.borrow().channels.is_empty(); + let has_channels = !unknown_grp.channels.is_empty(); if has_channels { - group_map.insert(Rc::new("0".to_string()), unknown_grp); + group_map.insert("0".to_string(), unknown_grp); } - Ok(Some(group_map.values().filter(|category| !category.borrow().channels.is_empty()) + Ok(Some(group_map.values().filter(|category| !category.channels.is_empty()) .map(|category| { - let cat = category.borrow(); PlaylistGroup { - id: cat.category_id.parse::().unwrap_or(0), + id: category.category_id.parse::().unwrap_or(0), xtream_cluster, - title: Rc::clone(&cat.category_name), - channels: cat.channels.clone(), + title: category.category_name.to_string(), + channels: category.channels.clone(), } }).collect())) } diff --git a/src/processing/playlist_watch.rs b/src/processing/playlist_watch.rs index 9f4478dbe..162b68043 100644 --- a/src/processing/playlist_watch.rs +++ b/src/processing/playlist_watch.rs @@ -11,7 +11,7 @@ use crate::utils::file::file_utils::sanitize_filename; pub fn process_group_watch(cfg: &Config, target_name: &str, pl: &PlaylistGroup) { let mut new_tree = BTreeSet::new(); pl.channels.iter().for_each(|chan| { - let header = chan.header.borrow(); + let header = &chan.header; let title = if header.title.is_empty() { header.title.to_string() } else { header.name.to_string() }; new_tree.insert(title); }); diff --git a/src/processing/processor/affix.rs b/src/processing/processor/affix.rs index ef633d04e..e68a135a1 100644 --- a/src/processing/processor/affix.rs +++ b/src/processing/processor/affix.rs @@ -6,7 +6,7 @@ type AffixProcessor<'a> = Box; fn create_affix_processor(affix: &InputAffix, is_prefix: bool) -> AffixProcessor { Box::new(move |channel: &mut PlaylistItem| { - let header = &mut channel.header.borrow_mut(); + let header = &mut channel.header; let value = header.get_field(affix.field.as_str()).map_or_else(|| String::from(&affix.value), |field_value| if is_prefix { format!("{}{}", &affix.value, field_value.as_str()) } else { diff --git a/src/processing/processor/playlist.rs b/src/processing/processor/playlist.rs index 1fbd8ea35..d0700fe9b 100644 --- a/src/processing/processor/playlist.rs +++ b/src/processing/processor/playlist.rs @@ -5,16 +5,13 @@ use crate::model::config::{ConfigInput, ConfigRename}; use crate::utils::network::epg; use crate::utils::network::m3u; use crate::utils::network::xtream; -use parking_lot::Mutex; use core::cmp::Ordering; -use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; -use std::rc::Rc; -use std::sync::Arc; +use std::sync::{Arc}; +use tokio::sync::Mutex; use std::thread; -use actix_rt::System; use log::{debug, error, info, log_enabled, trace, warn, Level}; use std::time::Instant; use unidecode::unidecode; @@ -37,7 +34,7 @@ use crate::utils::default_utils::default_as_default; use crate::utils::{debug_if_enabled}; fn is_valid(pli: &PlaylistItem, target: &ConfigTarget) -> bool { - let provider = ValueProvider { pli: RefCell::new(pli) }; + let provider = ValueProvider { pli }; target.filter(&provider) } @@ -62,8 +59,8 @@ fn filter_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt } fn playlistgroup_comparator(a: &PlaylistGroup, b: &PlaylistGroup, group_sort: &ConfigSortGroup, match_as_ascii: bool) -> Ordering { - let value_a = if match_as_ascii { Rc::new(unidecode(&a.title)) } else { Rc::clone(&a.title) }; - let value_b = if match_as_ascii { Rc::new(unidecode(&b.title)) } else { Rc::clone(&b.title) }; + let value_a = if match_as_ascii { unidecode(&a.title) } else { a.title.to_string() }; + let value_b = if match_as_ascii { unidecode(&b.title) } else { b.title.to_string() }; let ordering = value_a.partial_cmp(&value_b).unwrap(); match group_sort.order { Asc => ordering, @@ -74,8 +71,8 @@ fn playlistgroup_comparator(a: &PlaylistGroup, b: &PlaylistGroup, group_sort: &C fn playlistitem_comparator(a: &PlaylistItem, b: &PlaylistItem, channel_sort: &ConfigSortChannel, match_as_ascii: bool) -> Ordering { let raw_value_a = get_field_value(a, &channel_sort.field); let raw_value_b = get_field_value(b, &channel_sort.field); - let value_a = if match_as_ascii { Rc::new(unidecode(&raw_value_a)) } else { raw_value_a }; - let value_b = if match_as_ascii { Rc::new(unidecode(&raw_value_b)) } else { raw_value_b }; + let value_a = if match_as_ascii { unidecode(&raw_value_a) } else { raw_value_a }; + let value_b = if match_as_ascii { unidecode(&raw_value_b) } else { raw_value_b }; channel_sort.sequence.as_ref().map_or_else(|| { let ordering = value_a.partial_cmp(&value_b).unwrap(); match channel_sort.order { @@ -84,8 +81,8 @@ fn playlistitem_comparator(a: &PlaylistItem, b: &PlaylistItem, channel_sort: &Co } }, |custom_order| { // Check indices in the custom order vector - let index_a = custom_order.iter().position(|s| s == value_a.as_ref()); - let index_b = custom_order.iter().position(|s| s == value_b.as_ref()); + let index_a = custom_order.iter().position(|s| s == &value_a); + let index_b = custom_order.iter().position(|s| s == &value_b); match (index_a, index_b) { (Some(idx_a), Some(idx_b)) => { @@ -122,7 +119,7 @@ fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) { for channel_sort in channel_sorts { let regexp = channel_sort.re.as_ref().unwrap(); for group in new_playlist.iter_mut() { - let group_title = if match_as_ascii { Rc::new(unidecode(&group.title)) } else { Rc::clone(&group.title) }; + let group_title = if match_as_ascii { unidecode(&group.title) } else { group.title.to_string() }; if regexp.is_match(group_title.as_str()) { group.channels.sort_by(|chan1, chan2| playlistitem_comparator(chan1, chan2, channel_sort, match_as_ascii)); } @@ -132,17 +129,17 @@ fn sort_playlist(target: &ConfigTarget, new_playlist: &mut [PlaylistGroup]) { } } -fn channel_no_playlist(new_playlist: &[PlaylistGroup]) { +fn channel_no_playlist(new_playlist: &mut [PlaylistGroup]) { let mut chno = 1; for group in new_playlist { - for chan in &group.channels { - chan.header.borrow_mut().chno = Rc::new(chno.to_string()); + for chan in &mut group.channels { + chan.header.chno = chno.to_string(); chno += 1; } } } -fn exec_rename(pli: &PlaylistItem, rename: Option<&Vec>) { +fn exec_rename(pli: &mut PlaylistItem, rename: Option<&Vec>) { if let Some(renames) = rename { if !renames.is_empty() { let result = pli; @@ -153,7 +150,7 @@ fn exec_rename(pli: &PlaylistItem, rename: Option<&Vec>) { debug_if_enabled!("Renamed {}={} to {}", &r.field, value, cap); } let value = cap.into_owned(); - set_field_value(result, &r.field, Rc::new(value)); + set_field_value(result, &r.field, value); } } } @@ -170,7 +167,7 @@ fn rename_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Opt if matches!(r.field, ItemField::Group) { let cap = r.re.as_ref().unwrap().replace_all(&grp.title, &r.new_name); debug_if_enabled!("Renamed group {} to {} for {}", &grp.title, cap, target.name); - grp.title = Rc::new(cap.into_owned()); + grp.title = cap.into_owned(); } } @@ -193,19 +190,20 @@ macro_rules! apply_pattern { }}; } -fn map_channel(channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem { +fn map_channel(mut channel: PlaylistItem, mapping: &Mapping) -> PlaylistItem { if !mapping.mapper.is_empty() { - let header = channel.header.borrow(); - let channel_name = if mapping.match_as_ascii { Rc::new(unidecode(&header.name)) } else { header.name.clone() }; + let header = &channel.header; + let channel_name = if mapping.match_as_ascii { unidecode(&header.name) } else { header.name.to_string() }; if mapping.match_as_ascii && log_enabled!(Level::Trace) { trace!("Decoded {} for matching to {}", &header.name, &channel_name); }; - drop(header); - let ref_chan = RefCell::new(&channel); - let provider = ValueProvider { pli: ref_chan.clone() }; + // let ref_chan = &mut channel; + let ref_chan = &mut channel; let mut mock_processor = MockValueProcessor {}; for m in &mapping.mapper { - let mut processor = MappingValueProcessor { pli: ref_chan.clone(), mapper: m }; + let provider = ValueProvider { pli: &ref_chan.clone() }; + let mut processor = MappingValueProcessor { pli: ref_chan, mapper: m }; match &m.t_filter { Some(filter) => { + if filter.filter(&provider, &mut mock_processor) { apply_pattern!(&m.t_pattern, &provider, &mut processor); } @@ -235,15 +233,15 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option let mut grp_id: u32 = 0; for playlist_group in new_playlist { for channel in &playlist_group.channels { - let cluster = &channel.header.borrow().xtream_cluster; - let title = &channel.header.borrow().group; + let cluster = &channel.header.xtream_cluster; + let title = &channel.header.group; if let Some(grp) = new_groups.iter_mut().find(|x| *x.title == **title) { grp.channels.push(channel.clone()); } else { grp_id += 1; new_groups.push(PlaylistGroup { id: grp_id, - title: Rc::clone(title), + title: title.to_string(), channels: vec![channel.clone()], xtream_cluster: *cluster, }); @@ -256,29 +254,29 @@ fn map_playlist(playlist: &mut [PlaylistGroup], target: &ConfigTarget) -> Option } } -fn map_playlist_counter(target: &ConfigTarget, playlist: &[PlaylistGroup]) { +fn map_playlist_counter(target: &ConfigTarget, playlist: &mut [PlaylistGroup]) { if target.t_mapping.is_some() { let mut mock_processor = MockValueProcessor {}; let mappings = target.t_mapping.as_ref().unwrap(); for mapping in mappings { if let Some(counter_list) = &mapping.t_counter { for counter in counter_list { - for plg in playlist { - for channel in &plg.channels { - let provider = ValueProvider { pli: RefCell::new(channel) }; + for plg in &mut *playlist { + for channel in &mut plg.channels { + let provider = ValueProvider { pli: channel }; if counter.filter.filter(&provider, &mut mock_processor) { let cntval = counter.value.load(core::sync::atomic::Ordering::SeqCst); let new_value = if counter.modifier == CounterModifier::Assign { cntval.to_string() } else { - let value = channel.header.borrow_mut().get_field(&counter.field).map_or_else(String::new, |field_value| field_value.to_string()); + let value = channel.header.get_field(&counter.field).map_or_else(String::new, |field_value| field_value.to_string()); if counter.modifier == CounterModifier::Suffix { format!("{value}{}{cntval}", counter.concat) } else { format!("{cntval}{}{value}", counter.concat) } }; - channel.header.borrow_mut().set_field(&counter.field, new_value.as_str()); + channel.header.set_field(&counter.field, new_value.as_str()); counter.value.fetch_add(1, core::sync::atomic::Ordering::SeqCst); } } @@ -401,7 +399,7 @@ async fn process_sources(client: Arc, config: Arc, user for (index, _) in config.sources.iter().enumerate() { // We're using the file lock this way on purpose let source_lock_path = PathBuf::from(format!("source_{index}")); - let Ok(update_lock) = config.file_locks.try_write_lock(&source_lock_path) else { + let Ok(update_lock) = config.file_locks.try_write_lock(&source_lock_path).await else { warn!("The update operation for the source at index {index} was skipped because an update is already in progress."); continue; }; @@ -414,11 +412,13 @@ async fn process_sources(client: Arc, config: Arc, user let http_client = Arc::clone(&client); let handles = &mut handle_list; let process = move || { - System::new().block_on(async { + // TODO better way ? + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { let (input_stats, target_stats, mut res_errors) = process_source(Arc::clone(&http_client), cfg, index, usr_trgts).await; - shared_errors.lock().append(&mut res_errors); + shared_errors.lock().await.append(&mut res_errors); let process_stats = SourceStats::new(input_stats, target_stats); - shared_stats.lock().push(process_stats); + shared_stats.lock().await.push(process_stats); }); }; handles.push(thread::spawn(process)); @@ -427,9 +427,9 @@ async fn process_sources(client: Arc, config: Arc, user } } else { let (input_stats, target_stats, mut res_errors) = process_source(Arc::clone(&client), cfg, index, usr_trgts).await; - shared_errors.lock().append(&mut res_errors); + shared_errors.lock().await.append(&mut res_errors); let process_stats = SourceStats::new(input_stats, target_stats); - shared_stats.lock().push(process_stats); + shared_stats.lock().await.push(process_stats); } drop(update_lock); } @@ -482,9 +482,9 @@ fn execute_pipe<'a>(target: &ConfigTarget, pipe: &ProcessingPipe, fpl: &FetchedP fn flatten_groups(playlistgroups: Vec) -> Vec { let mut sort_order: Vec = vec![]; let mut idx: usize = 0; - let mut group_map: HashMap<(Rc, XtreamCluster), usize> = HashMap::new(); + let mut group_map: HashMap<(String, XtreamCluster), usize> = HashMap::new(); for group in playlistgroups { - let key = (Rc::clone(&group.title), group.xtream_cluster); + let key = (group.title.to_string(), group.xtream_cluster); match group_map.entry(key) { std::collections::hash_map::Entry::Vacant(v) => { v.insert(idx); @@ -513,7 +513,7 @@ async fn process_playlist_for_target(client: Arc, for provider_fpl in playlists.iter_mut() { let mut processed_fpl = execute_pipe(target, &pipe, provider_fpl, &mut duplicates); playlist_resolve_series(Arc::clone(&client), cfg, target, errors, &pipe, provider_fpl, &mut processed_fpl).await; - playlist_resolve_vod(Arc::clone(&client), cfg, target, errors, &processed_fpl).await; + playlist_resolve_vod(Arc::clone(&client), cfg, target, errors, &mut processed_fpl).await; // stats let input_stats = stats.get_mut(&processed_fpl.input.name); if let Some(stat) = input_stats { @@ -535,7 +535,7 @@ async fn process_playlist_for_target(client: Arc, for mut fp in processed_fetched_playlists { // collect all epg_channel ids let epg_channel_ids: HashSet<_> = fp.playlistgroups.iter().flat_map(|g| &g.channels) - .filter_map(|c| c.header.borrow().epg_channel_id.clone()).collect(); + .filter_map(|c| c.header.epg_channel_id.clone()).collect(); new_playlist.append(&mut fp.playlistgroups); if epg_channel_ids.is_empty() { @@ -554,8 +554,8 @@ async fn process_playlist_for_target(client: Arc, } else { let mut flat_new_playlist = flatten_groups(new_playlist); sort_playlist(target, &mut flat_new_playlist); - channel_no_playlist(&flat_new_playlist); - map_playlist_counter(target, &flat_new_playlist); + channel_no_playlist(&mut flat_new_playlist); + map_playlist_counter(target, &mut flat_new_playlist); process_watch(target, cfg, &flat_new_playlist); persist_playlist(&mut flat_new_playlist, flatten_tvguide(&new_epg).as_ref(), target, cfg).await } diff --git a/src/processing/processor/xtream.rs b/src/processing/processor/xtream.rs index 1e7cb518a..8fd2863cc 100644 --- a/src/processing/processor/xtream.rs +++ b/src/processing/processor/xtream.rs @@ -33,7 +33,7 @@ pub(in crate::processing) async fn playlist_resolve_download_playlist_item(clien }; } if resolve_delay > 0 { - actix_web::rt::time::sleep(std::time::Duration::new(u64::from(resolve_delay), 0)).await; + tokio::time::sleep(std::time::Duration::new(u64::from(resolve_delay), 0)).await; } result } @@ -79,9 +79,9 @@ pub(in crate::processing) fn create_resolve_info_wal_files(cfg: &Config, input: } } -pub(in crate::processing) fn should_update_info(pli: &PlaylistItem, processed_provider_ids: &HashMap, field: &str) -> (bool, u32, u64) { - let Some(provider_id) = pli.header.borrow_mut().get_provider_id() else { return (false, 0, 0) }; - let last_modified = pli.header.borrow().get_additional_property_as_u64(field); +pub(in crate::processing) fn should_update_info(pli: &mut PlaylistItem, processed_provider_ids: &HashMap, field: &str) -> (bool, u32, u64) { + let Some(provider_id) = pli.header.get_provider_id() else { return (false, 0, 0) }; + let last_modified = pli.header.get_additional_property_as_u64(field); let old_timestamp = processed_provider_ids.get(&provider_id); (old_timestamp.is_none() || last_modified.is_none() diff --git a/src/processing/processor/xtream_series.rs b/src/processing/processor/xtream_series.rs index fdcb2b904..10bb47f77 100644 --- a/src/processing/processor/xtream_series.rs +++ b/src/processing/processor/xtream_series.rs @@ -52,7 +52,7 @@ fn write_series_episode_record_to_wal_file( Ok(()) } -fn should_update_series_info(pli: &PlaylistItem, processed_provider_ids: &HashMap) -> (bool, u32, u64) { +fn should_update_series_info(pli: &mut PlaylistItem, processed_provider_ids: &HashMap) -> (bool, u32, u64) { should_update_info(pli, processed_provider_ids, TAG_SERIES_INFO_LAST_MODIFIED) } @@ -69,12 +69,18 @@ async fn playlist_resolve_series_info(client: Arc, cfg: &Config let mut record_writer = file_writer(&wal_record_file); let mut content_updated = false; - let series_info_iter = fpl.playlistgroups.iter() + // TODO merge both filters to one + let series_info_count = fpl.playlistgroups.iter() .filter(|&plg| plg.xtream_cluster == XtreamCluster::Series) .flat_map(|plg| &plg.channels) - .filter(|&pli| pli.header.borrow().item_type == PlaylistItemType::SeriesInfo); + .filter(|&pli| pli.header.item_type == PlaylistItemType::SeriesInfo).count(); + + let series_info_iter = fpl.playlistgroups.iter_mut() + .filter(|plg| plg.xtream_cluster == XtreamCluster::Series) + .flat_map(|plg| &mut plg.channels) + .filter(|pli| pli.header.item_type == PlaylistItemType::SeriesInfo); + - let series_info_count = series_info_iter.clone().count(); info!("Found {series_info_count} series info to resolve"); let start_time = Instant::now(); let mut processed_series_info_count = 0; @@ -160,21 +166,21 @@ async fn process_series_info( for pli in plg .channels - .iter() - .filter(|pli| pli.header.borrow().item_type == PlaylistItemType::SeriesInfo) + .iter_mut() + .filter(|pli| pli.header.item_type == PlaylistItemType::SeriesInfo) { - let Some(provider_id) = pli.header.borrow_mut().get_provider_id() else { continue; }; + let Some(provider_id) = pli.header.get_provider_id() else { continue; }; let Ok(content) = info_reader.get(&provider_id) else { continue; }; match serde_json::from_str::(&content) { Ok(series_content) => { let (group, series_name) = { - let header = pli.header.borrow(); + let header = &pli.header; (header.group.clone(), if header.name.is_empty() {header.title.clone()} else { header.name.clone()}) }; match parse_xtream_series_info(&series_content, &group, &series_name, input) { - Ok(Some(series)) => { - for (episode, pli_episode) in &series { - let Some(provider_id) = &pli_episode.header.borrow_mut().get_provider_id() else { continue; }; + Ok(Some(mut series)) => { + for (episode, pli_episode) in &mut series { + let Some(provider_id) = &pli_episode.header.get_provider_id() else { continue; }; handle_error!(write_series_episode_record_to_wal_file(&mut wal_writer, *provider_id, episode), |err| errors.push(info_err!(format!("Failed to write to series episode wal file: {err}")))); } diff --git a/src/processing/processor/xtream_vod.rs b/src/processing/processor/xtream_vod.rs index 8acc10a97..7aade4229 100644 --- a/src/processing/processor/xtream_vod.rs +++ b/src/processing/processor/xtream_vod.rs @@ -62,11 +62,11 @@ fn write_vod_info_record_to_wal_file( Ok(()) } -fn should_update_vod_info(pli: &PlaylistItem, processed_provider_ids: &HashMap) -> (bool, u32, u64) { +fn should_update_vod_info(pli: &mut PlaylistItem, processed_provider_ids: &HashMap) -> (bool, u32, u64) { should_update_info(pli, processed_provider_ids, TAG_VOD_INFO_ADDED) } -pub async fn playlist_resolve_vod(client: Arc, cfg: &Config, target: &ConfigTarget, errors: &mut Vec, fpl: &FetchedPlaylist<'_>) { +pub async fn playlist_resolve_vod(client: Arc, cfg: &Config, target: &ConfigTarget, errors: &mut Vec, fpl: &mut FetchedPlaylist<'_>) { let (resolve_movies, resolve_delay) = get_resolve_vod_options(target, fpl); if !resolve_movies { return; } @@ -81,11 +81,14 @@ pub async fn playlist_resolve_vod(client: Arc, cfg: &Config, ta let mut record_writer = file_writer(&wal_record_file); let mut content_updated = false; - let vod_info_iter = fpl.playlistgroups.iter() + // TODO merge both filters to one + let vod_info_count = fpl.playlistgroups.iter() .flat_map(|plg| &plg.channels) - .filter(|&pli| pli.header.borrow().xtream_cluster == XtreamCluster::Video); + .filter(|&pli| pli.header.xtream_cluster == XtreamCluster::Video).count(); - let vod_info_count = vod_info_iter.clone().count(); + let vod_info_iter = fpl.playlistgroups.iter_mut() + .flat_map(|plg| plg.channels.iter_mut()) + .filter(|pli| pli.header.xtream_cluster == XtreamCluster::Video); info!("Found {vod_info_count} vod info to resolve"); let start_time = Instant::now(); diff --git a/src/repository/kodi_repository.rs b/src/repository/kodi_repository.rs index aac0288b8..c23d4fed3 100644 --- a/src/repository/kodi_repository.rs +++ b/src/repository/kodi_repository.rs @@ -21,8 +21,7 @@ use regex::Regex; use serde::Serialize; use std::collections::{HashMap, HashSet, VecDeque}; use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::sync::LazyLock; +use std::sync::{Arc, LazyLock}; use tokio::fs::{create_dir_all, remove_dir, remove_file, File}; use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}; @@ -155,7 +154,7 @@ fn trim_whitespace(pattern: &Regex, input: &str) -> String { pattern.replace_all(input, " ").to_string() } -fn kodi_style_rename( +async fn kodi_style_rename( cfg: &Config, strm_item_info: &StrmItemInfo, style: &KodiStyle, @@ -189,7 +188,7 @@ fn kodi_style_rename( strm_item_info.input_name.as_str(), input_tmdb_indexes, strm_item_info.item_type, - ) + ).await } _ => None, } { @@ -280,7 +279,7 @@ enum InputTmdbIndexValue { } type InputTmdbIndexMap = HashMap>; -fn get_tmdb_value( +async fn get_tmdb_value( cfg: &Config, provider_id: Option, input_name: &str, @@ -311,7 +310,7 @@ fn get_tmdb_value( .map(|storage_path| xtream_get_record_file_path(&storage_path, item_type)) { { - let file_lock = cfg.file_locks.read_lock(&tmdb_path); + let file_lock = cfg.file_locks.read_lock(&tmdb_path).await; match item_type { PlaylistItemType::Series => { if let Ok(tree) = @@ -358,13 +357,13 @@ pub fn strm_get_file_paths(target_path: &Path) -> PathBuf { #[derive(Serialize)] struct StrmItemInfo { - group: Rc, - title: Rc, + group: String, + title: String, item_type: PlaylistItemType, provider_id: Option, virtual_id: u32, - input_name: Rc, - url: Rc, + input_name: String, + url: String, series_name: Option, release_date: Option, season: Option, @@ -378,15 +377,15 @@ impl StrmItemInfo { } } -fn extract_item_info(pli: &PlaylistItem) -> StrmItemInfo { - let mut header = pli.header.borrow_mut(); - let group = Rc::clone(&header.group); - let title = Rc::clone(&header.title); +fn extract_item_info(pli: &mut PlaylistItem) -> StrmItemInfo { + let header = &mut pli.header; + let group = header.group.to_string(); + let title = header.title.to_string(); let item_type = header.item_type; let provider_id = header.get_provider_id(); let virtual_id = header.virtual_id; - let input_name = Rc::clone(&header.input_name); - let url = Rc::clone(&header.url); + let input_name = header.input_name.to_string(); + let url = header.url.to_string(); let (series_name, release_date, added, season, episode) = match header.item_type { PlaylistItemType::Series => { let series_name = match header.get_field("name") { @@ -502,7 +501,7 @@ async fn cleanup_strm_output_directory( } fn filter_strm_item(pli: &PlaylistItem) -> bool { - let item_type = pli.header.borrow().item_type; + let item_type = pli.header.item_type; item_type == PlaylistItemType::Series || item_type == PlaylistItemType::Live || item_type == PlaylistItemType::Video @@ -519,14 +518,14 @@ fn get_relative_path_str(full_path: &Path, root_path: &Path) -> String { } struct StrmFile { - file_name: Rc, + file_name: Arc, dir_path: PathBuf, strm_info: StrmItemInfo, } -fn prepare_strm_files( +async fn prepare_strm_files( cfg: &Config, - new_playlist: &[PlaylistGroup], + new_playlist: &mut [PlaylistGroup], root_path: &Path, underscore_whitespace: bool, kodi_style: bool, @@ -538,13 +537,13 @@ fn prepare_strm_files( // contains all filenames to detect collisions let mut all_filenames = HashSet::with_capacity(channel_count); // contains only collision filenames - let mut collisions: HashSet> = HashSet::new(); + let mut collisions: HashSet> = HashSet::new(); let mut input_tmdb_indexes: InputTmdbIndexMap = HashMap::with_capacity(channel_count); let mut result = Vec::with_capacity(channel_count); // first we create the names to identify name collisions - for pg in new_playlist { - for pli in pg.channels.iter().filter(|&c| filter_strm_item(c)) { + for pg in new_playlist.iter_mut() { + for pli in pg.channels.iter_mut().filter(|c| filter_strm_item(c)) { let strm_item_info = extract_item_info(pli); let (dir_path, strm_file_name) = if kodi_style { kodi_style_rename( @@ -553,7 +552,7 @@ fn prepare_strm_files( &KODI_STYLE, &mut input_tmdb_indexes, underscore_whitespace, - ) + ).await } else { let dir_path = root_path.join(sanitize_for_filename( &strm_item_info.group, @@ -563,13 +562,13 @@ fn prepare_strm_files( sanitize_for_filename(&strm_item_info.title, underscore_whitespace); (dir_path, strm_file_name) }; - let filename = Rc::new(strm_file_name); + let filename = Arc::new(strm_file_name); if all_filenames.contains(&filename) { - collisions.insert(Rc::clone(&filename)); + collisions.insert(Arc::clone(&filename)); }; - all_filenames.insert(Rc::clone(&filename)); + all_filenames.insert(Arc::clone(&filename)); result.push(StrmFile { - file_name: Rc::clone(&filename), + file_name: Arc::clone(&filename), dir_path, strm_info: strm_item_info, }); @@ -582,7 +581,7 @@ fn prepare_strm_files( .iter_mut() .filter(|s| collisions.contains(&s.file_name)) .for_each(|s| { - s.file_name = Rc::new(format!( + s.file_name = Arc::new(format!( "{}{separator}-{separator}[{}]", s.file_name, s.strm_info.virtual_id )); @@ -595,7 +594,7 @@ pub async fn kodi_write_strm_playlist( target: &ConfigTarget, target_output: &StrmTargetOutput, cfg: &Config, - new_playlist: &[PlaylistGroup], + new_playlist: &mut [PlaylistGroup], ) -> Result<(), M3uFilterError> { if new_playlist.is_empty() { return Ok(()); @@ -611,7 +610,7 @@ pub async fn kodi_write_strm_playlist( ))); }; - let credentials_and_server_info = get_credentials_and_server_info(cfg, target_output.username.as_ref()); + let credentials_and_server_info = get_credentials_and_server_info(cfg, target_output.username.as_ref()).await; let strm_index_path = strm_get_file_paths(&ensure_target_storage_path(cfg, target.name.as_str())?); let existing_strm = { @@ -640,7 +639,7 @@ pub async fn kodi_write_strm_playlist( &root_path, target_output.underscore_whitespace, target_output.kodi_style, - ); + ).await; for strm_file in strm_files { // file paths let output_path = root_path.join(&strm_file.dir_path); @@ -783,16 +782,16 @@ async fn has_strm_file_same_hash(file_path: &PathBuf, content_hash: UUIDType) -> false } -fn get_credentials_and_server_info( +async fn get_credentials_and_server_info( cfg: &Config, username: Option<&String>, ) -> Option<(ProxyUserCredentials, ApiProxyServerInfo)> { let username = username?; - let credentials = cfg.get_user_credentials(username)?; + let credentials = cfg.get_user_credentials(username).await?; if credentials.proxy != ProxyType::Reverse { return None; } - let server_info = cfg.get_user_server_info(&credentials); + let server_info = cfg.get_user_server_info(&credentials).await; Some((credentials, server_info)) } diff --git a/src/repository/m3u_playlist_iterator.rs b/src/repository/m3u_playlist_iterator.rs index 6475d2329..14e1b665a 100644 --- a/src/repository/m3u_playlist_iterator.rs +++ b/src/repository/m3u_playlist_iterator.rs @@ -38,7 +38,7 @@ impl M3uPlaylistIterator { let target_path = ensure_target_storage_path(cfg, target.name.as_str())?; let (m3u_path, idx_path) = m3u_get_file_paths(&target_path); - let file_lock = cfg.file_locks.read_lock(&m3u_path); + let file_lock = cfg.file_locks.read_lock(&m3u_path).await; let reader = IndexedDocumentIterator::::new(&m3u_path, &idx_path) @@ -47,7 +47,7 @@ impl M3uPlaylistIterator { let filter = user_get_bouquet_filter(cfg, &user.username, None, TargetType::M3u, XtreamCluster::Live).await; // TODO m3u bouquet filter - let server_info = cfg.get_user_server_info(user); + let server_info = cfg.get_user_server_info(user).await; Ok(Self { reader, base_url: server_info.get_base_url(), diff --git a/src/repository/m3u_repository.rs b/src/repository/m3u_repository.rs index f4907b566..8d68bc5bc 100644 --- a/src/repository/m3u_repository.rs +++ b/src/repository/m3u_repository.rs @@ -58,7 +58,7 @@ pub async fn m3u_write_playlist( cfg: &Config, target: &ConfigTarget, target_out let (m3u_path, idx_path) = m3u_get_file_paths(target_path); let m3u_playlist = new_playlist.iter() .flat_map(|pg| &pg.channels) - .filter(|&pli| pli.header.borrow().item_type != PlaylistItemType::SeriesInfo) + .filter(|&pli| pli.header.item_type != PlaylistItemType::SeriesInfo) .map(PlaylistItem::to_m3u).collect::>(); persist_m3u_playlist_as_text(cfg, target, target_output, &m3u_playlist); @@ -101,13 +101,13 @@ pub async fn m3u_get_item_for_stream_id(stream_id: u32, cfg: &Config, target: &C } } -pub fn iter_raw_m3u_playlist(config: &Arc, target: &ConfigTarget) -> Option<(FileReadGuard, impl Iterator)> { +pub async fn iter_raw_m3u_playlist(config: &Arc, target: &ConfigTarget) -> Option<(FileReadGuard, impl Iterator)> { let target_path = get_target_storage_path(config, target.name.as_str())?; let (m3u_path, idx_path) = m3u_get_file_paths(&target_path); if !m3u_path.exists() || !idx_path.exists() { return None; } - let file_lock = config.file_locks.read_lock(&m3u_path); + let file_lock = config.file_locks.read_lock(&m3u_path).await; match IndexedDocumentIterator::::new(&m3u_path, &idx_path) .map_err(|err| info_err!(format!("Could not deserialize file {m3u_path:?} - {err}"))) { Ok(reader) => Some((file_lock, reader)), diff --git a/src/repository/playlist_repository.rs b/src/repository/playlist_repository.rs index 925e00e72..ef6031d6f 100644 --- a/src/repository/playlist_repository.rs +++ b/src/repository/playlist_repository.rs @@ -22,12 +22,12 @@ pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>, Err(err) => return Err(vec![err]), }; - let (mut target_id_mapping, file_lock) = get_target_id_mapping(cfg, &target_path); + let (mut target_id_mapping, file_lock) = get_target_id_mapping(cfg, &target_path).await; // Virtual IDs assignment for group in playlist.iter_mut() { - for channel in &group.channels { - let mut header = channel.header.borrow_mut(); + for channel in &mut group.channels { + let header = &mut channel.header; let provider_id = header.get_provider_id().unwrap_or_default(); if provider_id == 0 { header.item_type = match (header.url.ends_with(HLS_EXT), header.item_type) { @@ -38,7 +38,7 @@ pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>, } let uuid = header.get_uuid(); let item_type = header.item_type; - header.virtual_id = target_id_mapping.get_and_update_virtual_id(**uuid, provider_id, item_type, 0); + header.virtual_id = target_id_mapping.get_and_update_virtual_id(uuid, provider_id, item_type, 0); } } @@ -67,8 +67,8 @@ pub async fn persist_playlist(playlist: &mut [PlaylistGroup], epg: Option<&Epg>, if errors.is_empty() { Ok(()) } else { Err(errors) } } -pub fn get_target_id_mapping(cfg: &Config, target_path: &Path) -> (TargetIdMapping, FileWriteGuard) { +pub async fn get_target_id_mapping(cfg: &Config, target_path: &Path) -> (TargetIdMapping, FileWriteGuard) { let target_id_mapping_file = get_target_id_mapping_file(target_path); - let file_lock = cfg.file_locks.write_lock(&target_id_mapping_file); + let file_lock = cfg.file_locks.write_lock(&target_id_mapping_file).await; (TargetIdMapping::new(&target_id_mapping_file), file_lock) } diff --git a/src/repository/target_id_mapping.rs b/src/repository/target_id_mapping.rs index 1f95336bd..94590c9e6 100644 --- a/src/repository/target_id_mapping.rs +++ b/src/repository/target_id_mapping.rs @@ -84,20 +84,20 @@ impl TargetIdMapping { // } // } - pub fn get_and_update_virtual_id(&mut self, uuid: UUIDType, provider_id: u32, item_type: PlaylistItemType, parent_virtual_id: u32) -> u32 { - match self.by_uuid.get(&uuid) { + pub fn get_and_update_virtual_id(&mut self, uuid: &UUIDType, provider_id: u32, item_type: PlaylistItemType, parent_virtual_id: u32) -> u32 { + match self.by_uuid.get(uuid) { None => { self.dirty = true; self.virtual_id_counter += 1; let virtual_id = self.virtual_id_counter; - let record = VirtualIdRecord::new(provider_id, virtual_id, item_type, parent_virtual_id, uuid); + let record = VirtualIdRecord::new(provider_id, virtual_id, item_type, parent_virtual_id, *uuid); self.by_virtual_id.insert(virtual_id, record); self.virtual_id_counter } Some(virtual_id) => { if let Some(record) = self.by_virtual_id.query(virtual_id) { if record.provider_id == provider_id && (record.item_type != item_type || record.parent_virtual_id != parent_virtual_id) { - let new_record = VirtualIdRecord::new(provider_id, *virtual_id, item_type, parent_virtual_id, uuid); + let new_record = VirtualIdRecord::new(provider_id, *virtual_id, item_type, parent_virtual_id, *uuid); self.by_virtual_id.insert(*virtual_id, new_record); self.dirty = true; } diff --git a/src/repository/xtream_playlist_iterator.rs b/src/repository/xtream_playlist_iterator.rs index 2a85adc49..ef66b1103 100644 --- a/src/repository/xtream_playlist_iterator.rs +++ b/src/repository/xtream_playlist_iterator.rs @@ -34,13 +34,13 @@ impl XtreamPlaylistIterator { if !xtream_path.exists() || !idx_path.exists() { return Err(info_err!(format!("No {cluster} entries found for target {}", &target.name))); } - let file_lock = config.file_locks.read_lock(&xtream_path); + let file_lock = config.file_locks.read_lock(&xtream_path).await; let reader = IndexedDocumentIterator::::new(&xtream_path, &idx_path) .map_err(|err| info_err!(format!("Could not deserialize file {xtream_path:?} - {err}")))?; let options = XtreamMappingOptions::from_target_options(xtream_output, config); - let server_info = config.get_user_server_info(user); + let server_info = config.get_user_server_info(user).await; let filter = user_get_bouquet_filter(config, &user.username, category_id, TargetType::Xtream, cluster).await; diff --git a/src/repository/xtream_repository.rs b/src/repository/xtream_repository.rs index b8f17d2db..1ef47152e 100644 --- a/src/repository/xtream_repository.rs +++ b/src/repository/xtream_repository.rs @@ -127,7 +127,7 @@ pub fn xtream_get_record_file_path(storage_path: &Path, item_type: PlaylistItemT fn write_playlists_to_file( cfg: &Config, storage_path: &Path, - collections: Vec<(XtreamCluster, &mut [&PlaylistItem])>, + collections: Vec<(XtreamCluster, &[&mut PlaylistItem])>, ) -> Result<(), M3uFilterError> { for (cluster, playlist) in collections { let (xtream_path, idx_path) = xtream_get_file_paths(storage_path, cluster); @@ -137,7 +137,7 @@ fn write_playlists_to_file( Ok(mut writer) => { for item in playlist { let xtream = item.to_xtream(); - match writer.write_doc(item.header.borrow().virtual_id, &xtream) { + match writer.write_doc(item.header.virtual_id, &xtream) { Ok(()) => {} Err(err) => return Err(cant_write_result!(&xtream_path, err)), } @@ -215,7 +215,7 @@ fn xtream_garbage_collect(config: &Config, target_name: &str) -> std::io::Result )); { let _file_lock = config.file_locks.write_lock(&info_path); - IndexedDocumentGarbageCollector::::new(info_path, idx_path)?.garbage_collect()?; + IndexedDocumentGarbageCollector::::new(info_path.clone(), idx_path)?.garbage_collect()?; } Ok(()) } @@ -256,16 +256,14 @@ pub async fn xtream_write_playlist( TAG_PARENT_ID: 0 })); - for pli in &plg.channels { - let mut header = pli.header.borrow_mut(); + for pli in &mut plg.channels { + let header = &mut pli.header; header.category_id = *cat_id; let col = match header.xtream_cluster { XtreamCluster::Live => &mut live_col, XtreamCluster::Series => &mut series_col, XtreamCluster::Video => &mut vod_col, }; - - drop(header); col.push(pli); } } @@ -288,9 +286,9 @@ pub async fn xtream_write_playlist( cfg, &path, vec![ - (XtreamCluster::Live, &mut live_col), - (XtreamCluster::Video, &mut vod_col), - (XtreamCluster::Series, &mut series_col), + (XtreamCluster::Live, &live_col), + (XtreamCluster::Video, &vod_col), + (XtreamCluster::Series, &series_col), ], ) { Ok(()) => { @@ -430,7 +428,7 @@ pub fn xtream_write_series_info( { let _file_lock = config.file_locks.write_lock(&info_path); - let mut writer = IndexedDocumentWriter::new_append(info_path, idx_path)?; + let mut writer = IndexedDocumentWriter::new_append(info_path.clone(), idx_path)?; writer.write_doc(series_info_id, content).map_err(|_| str_to_io_error(&format!("failed to write xtream series info for target {target_name}")))?; writer.store()?; } @@ -448,7 +446,7 @@ pub fn xtream_write_series_info( Ok(()) } -pub fn xtream_write_vod_info( +pub async fn xtream_write_vod_info( config: &Config, target_name: &str, virtual_id: u32, @@ -457,8 +455,8 @@ pub fn xtream_write_vod_info( let storage_path = try_option_ok!(xtream_get_storage_path(config, target_name)); let (info_path, idx_path) = try_option_ok!(xtream_get_info_file_paths(&storage_path, XtreamCluster::Video)); { - let _file_lock = config.file_locks.write_lock(&info_path); - let mut writer = IndexedDocumentWriter::new_append(info_path, idx_path)?; + let _file_lock = config.file_locks.write_lock(&info_path).await; + let mut writer = IndexedDocumentWriter::new_append(info_path.clone(), idx_path)?; writer.write_doc(virtual_id, content).map_err(|_| str_to_io_error(&format!("failed to write xtream vod info for target {target_name}")))?; writer.store()?; } @@ -549,7 +547,7 @@ pub fn xtream_load_vod_info( None } -fn rewrite_xtream_vod_info

( +async fn rewrite_xtream_vod_info

( config: &Config, xtream_output: &XtreamTargetOutput, pli: &P, @@ -563,7 +561,7 @@ fn rewrite_xtream_vod_info

( if let Some(Value::Object(info_data)) = doc.get_mut(TAG_INFO_DATA) { match user.proxy { ProxyType::Reverse => { - let server_info = config.get_user_server_info(user); + let server_info = config.get_user_server_info(user).await; let url = server_info.get_base_url(); let resource_url = Some(format!("{url}/resource/movie/{}/{}/{}", user.username, user.password, pli.get_virtual_id())); rewrite_doc_urls(resource_url.as_ref(), info_data, INFO_REWRITE_FIELDS, INFO_RESOURCE_PREFIX); @@ -596,7 +594,7 @@ fn rewrite_xtream_vod_info

( Ok(result) } -pub fn rewrite_xtream_vod_info_content

( +pub async fn rewrite_xtream_vod_info_content

( config: &Config, xtream_output: &XtreamTargetOutput, pli: &P, @@ -606,10 +604,10 @@ pub fn rewrite_xtream_vod_info_content

( P: PlaylistEntry, { let mut doc = serde_json::from_str::>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?; - rewrite_xtream_vod_info(config, xtream_output, pli, user, &mut doc) + rewrite_xtream_vod_info(config, xtream_output, pli, user, &mut doc).await } -pub fn write_and_get_xtream_vod_info

( +pub async fn write_and_get_xtream_vod_info

( config: &Config, target: &ConfigTarget, xtream_output: &XtreamTargetOutput, @@ -620,11 +618,11 @@ pub fn write_and_get_xtream_vod_info

( P: PlaylistEntry, { let mut doc = serde_json::from_str::>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?; - xtream_write_vod_info(config, target.name.as_str(), pli.get_virtual_id(), content).ok(); - rewrite_xtream_vod_info(config, xtream_output, pli, user, &mut doc) + xtream_write_vod_info(config, target.name.as_str(), pli.get_virtual_id(), content).await.ok(); + rewrite_xtream_vod_info(config, xtream_output, pli, user, &mut doc).await } -fn rewrite_xtream_series_info

( +async fn rewrite_xtream_series_info

( config: &Config, target: &ConfigTarget, xtream_output: &XtreamTargetOutput, @@ -639,7 +637,7 @@ fn rewrite_xtream_series_info

( let resource_url = if config.is_reverse_proxy_resource_rewrite_enabled() { match user.proxy { ProxyType::Reverse => { - let server_info = config.get_user_server_info(user); + let server_info = config.get_user_server_info(user).await; let url = server_info.get_base_url(); Some(format!("{url}/resource/series/{}/{}/{}", user.username, user.password, pli.get_virtual_id())) } @@ -669,7 +667,7 @@ fn rewrite_xtream_series_info

( let virtual_id = pli.get_virtual_id(); { - let (mut target_id_mapping, file_lock) = get_target_id_mapping(config, &target_path); + let (mut target_id_mapping, file_lock) = get_target_id_mapping(config, &target_path).await; let options = XtreamMappingOptions::from_target_options(xtream_output, config); let provider_url = pli.get_provider_url(); @@ -679,7 +677,7 @@ fn rewrite_xtream_series_info

( { let uuid = generate_playlist_uuid(&hex_encode(&pli.get_uuid()), &episode_provider_id.to_string(), PlaylistItemType::Series, &provider_url); let episode_virtual_id = target_id_mapping.get_and_update_virtual_id( - uuid, + &uuid, episode_provider_id, PlaylistItemType::Series, virtual_id, @@ -710,7 +708,7 @@ fn rewrite_xtream_series_info

( Ok(result) } -pub fn rewrite_xtream_series_info_content

( +pub async fn rewrite_xtream_series_info_content

( config: &Config, target: &ConfigTarget, xtream_output: &XtreamTargetOutput, @@ -721,10 +719,10 @@ pub fn rewrite_xtream_series_info_content

( P: PlaylistEntry, { let mut doc = serde_json::from_str::>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?; - rewrite_xtream_series_info(config, target, xtream_output, pli_series_info, user, &mut doc) + rewrite_xtream_series_info(config, target, xtream_output, pli_series_info, user, &mut doc).await } -pub fn write_and_get_xtream_series_info

( +pub async fn write_and_get_xtream_series_info

( config: &Config, target: &ConfigTarget, xtream_output: &XtreamTargetOutput, @@ -737,7 +735,7 @@ pub fn write_and_get_xtream_series_info

( let mut doc = serde_json::from_str::>(content).map_err(|_| str_to_io_error("Failed to parse JSON content"))?; let virtual_id = pli_series_info.get_virtual_id(); xtream_write_series_info(config, target.name.as_str(), virtual_id, content).ok(); - rewrite_xtream_series_info(config, target, xtream_output, pli_series_info, user, &mut doc) + rewrite_xtream_series_info(config, target, xtream_output, pli_series_info, user, &mut doc).await } pub fn xtream_get_input_info( @@ -767,7 +765,7 @@ pub async fn xtream_update_input_info_file( { let _file_lock = cfg.file_locks.write_lock(&info_path); let mut reader = file_reader(open_readonly_file(wal_path).map_err(|err| notify_err!(format!("Could not read {cluster} info {err}")))?); - match IndexedDocumentWriter::::new_append(info_path, idx_path) { + match IndexedDocumentWriter::::new_append(info_path.clone(), idx_path) { Ok(mut writer) => { let mut provider_id_bytes = [0u8; 4]; let mut length_bytes = [0u8; 4]; @@ -925,13 +923,13 @@ pub async fn xtream_update_input_series_episodes_record_from_wal_file( } } -pub fn iter_raw_xtream_playlist(config: &Arc, target: &ConfigTarget, cluster: XtreamCluster) -> Option<(FileReadGuard, impl Iterator)> { +pub async fn iter_raw_xtream_playlist(config: &Arc, target: &ConfigTarget, cluster: XtreamCluster) -> Option<(FileReadGuard, impl Iterator)> { if let Some(storage_path) = xtream_get_storage_path(config, target.name.as_str()) { let (xtream_path, idx_path) = xtream_get_file_paths(&storage_path, cluster); if !xtream_path.exists() || !idx_path.exists() { return None; } - let file_lock = config.file_locks.read_lock(&xtream_path); + let file_lock = config.file_locks.read_lock(&xtream_path).await; match IndexedDocumentIterator::::new(&xtream_path, &idx_path) .map_err(|err| info_err!(format!("Could not deserialize file {xtream_path:?} - {err}"))) { Ok(reader) => Some((file_lock, reader)), diff --git a/src/tools/lru_cache.rs b/src/tools/lru_cache.rs index 150860f1e..84d9f541c 100644 --- a/src/tools/lru_cache.rs +++ b/src/tools/lru_cache.rs @@ -1,11 +1,11 @@ use crate::repository::storage::hash_string_as_hex; use crate::utils::file::file_utils::traverse_dir; use crate::utils::size_utils::human_readable_byte_size; -use parking_lot::RwLock; use log::{debug, error, info, trace}; use std::collections::{HashMap, VecDeque}; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::RwLock; /// `LRUResourceCache` /// diff --git a/src/utils/file/config_reader.rs b/src/utils/file/config_reader.rs index a7348863e..620601dcf 100644 --- a/src/utils/file/config_reader.rs +++ b/src/utils/file/config_reader.rs @@ -33,7 +33,7 @@ pub fn read_mappings(args_mapping: Option, cfg: &mut Config) -> Result, cfg: &mut Config) -> Result, M3uFilterError> { +pub async fn read_api_proxy_config(args_api_proxy_config: Option, cfg: &mut Config) -> Result, M3uFilterError> { let api_proxy_config_file: String = args_api_proxy_config.unwrap_or_else(|| file_utils::get_default_api_proxy_config_path(cfg.t_config_path.as_str())); api_proxy_config_file.clone_into(&mut cfg.t_api_proxy_file_path); let api_proxy_config = read_api_proxy(cfg, api_proxy_config_file.as_str(), true); @@ -43,7 +43,7 @@ pub fn read_api_proxy_config(args_api_proxy_config: Option, cfg: &mut Co Ok(None) } Some(config) => { - cfg.set_api_proxy(Some(config))?; + cfg.set_api_proxy(Some(config)).await?; Ok(Some(api_proxy_config_file)) } } diff --git a/src/utils/file/file_lock_manager.rs b/src/utils/file/file_lock_manager.rs index 280b362c2..84bfe58cf 100644 --- a/src/utils/file/file_lock_manager.rs +++ b/src/utils/file/file_lock_manager.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::{fmt, io}; use std::path::{Path, PathBuf}; -use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tokio::sync::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard}; use crate::m3u_filter_error::str_to_io_error; #[derive(Clone)] @@ -18,36 +18,36 @@ impl FileLockManager { } // Acquires a read lock for the specified file and returns a FileReadGuard. - pub fn read_lock(&self, path: &Path) -> FileReadGuard { - let file_lock = self.get_or_create_lock(path); - let guard = file_lock.read(); + pub async fn read_lock(&self, path: &Path) -> FileReadGuard { + let file_lock = self.get_or_create_lock(path).await; + let guard = file_lock.read().await; // Clone the Arc to avoid moving `file_lock` out, as it is still borrowed by `guard` FileReadGuard::new(Arc::clone(&file_lock), guard) } // Acquires a write lock for the specified file and returns a FileWriteGuard. - pub fn write_lock(&self, path: &Path) -> FileWriteGuard { - let file_lock = self.get_or_create_lock(path); - let guard = file_lock.write(); + pub async fn write_lock(&self, path: &Path) -> FileWriteGuard { + let file_lock = self.get_or_create_lock(path).await; + let guard = file_lock.write().await; // Clone the Arc to avoid moving `file_lock` out, as it is still borrowed by `guard` FileWriteGuard::new(Arc::clone(&file_lock), guard) } // Tries to acquire a write lock for the specified file and returns a FileWriteGuard. - pub fn try_write_lock(&self, path: &Path) -> io::Result { - let file_lock = self.get_or_create_lock(path); + pub async fn try_write_lock(&self, path: &Path) -> io::Result { + let file_lock = self.get_or_create_lock(path).await; let guard = file_lock.try_write(); match guard { // Clone the Arc to avoid moving `file_lock` out, as it is still borrowed by `guard` - Some(lock_guard) => Ok(FileWriteGuard::new(Arc::clone(&file_lock), lock_guard)), - None => Err(str_to_io_error("Failed to acquire write lock")) + Ok(lock_guard) => Ok(FileWriteGuard::new(Arc::clone(&file_lock), lock_guard)), + Err(_) => Err(str_to_io_error("Failed to acquire write lock")) } } // Helper function: retrieves or creates a lock for a file. - fn get_or_create_lock(&self, path: &Path) -> Arc> { - let mut locks = self.locks.lock(); + async fn get_or_create_lock(&self, path: &Path) -> Arc> { + let mut locks = self.locks.lock().await; if let Some(lock) = locks.get(path) { return lock.clone(); @@ -75,10 +75,11 @@ impl fmt::Debug for FileLockManager { } // Define FileReadGuard to hold both the lock reference and the actual read guard. +#[derive(Clone)] #[allow(dead_code)] pub struct FileReadGuard { lock: Arc>, - guard: RwLockReadGuard<'static, ()>, + guard: Arc>, } impl FileReadGuard { @@ -87,16 +88,17 @@ impl FileReadGuard { let static_guard: RwLockReadGuard<'static, ()> = unsafe { std::mem::transmute(guard) }; Self { lock, - guard: static_guard, + guard: Arc::new(static_guard), } } } // Define FileWriteGuard to hold both the lock reference and the actual write guard. +#[derive(Clone)] #[allow(dead_code)] pub struct FileWriteGuard { lock: Arc>, - guard: RwLockWriteGuard<'static, ()>, + guard: Arc>, } impl FileWriteGuard { @@ -105,7 +107,7 @@ impl FileWriteGuard { let static_guard: RwLockWriteGuard<'static, ()> = unsafe { std::mem::transmute(guard) }; Self { lock, - guard: static_guard, + guard: Arc::new(static_guard), } } } diff --git a/src/utils/network/request.rs b/src/utils/network/request.rs index a057730c5..ec0803d20 100644 --- a/src/utils/network/request.rs +++ b/src/utils/network/request.rs @@ -413,7 +413,7 @@ pub enum MimeCategory { pub fn classify_content_type(headers: &[(String, String)]) -> MimeCategory { headers.iter() .find_map(|(k, v)| { - (k == actix_web::http::header::CONTENT_TYPE.as_str()).then_some(v) + (k == axum::http::header::CONTENT_TYPE.as_str()).then_some(v) }) .map_or(MimeCategory::Unknown, |v| match v.to_lowercase().as_str() { v if v.starts_with("video/") || v == "application/octet-stream" => MimeCategory::Video, diff --git a/src/utils/network/xtream.rs b/src/utils/network/xtream.rs index c64d43a3d..3f0af6883 100644 --- a/src/utils/network/xtream.rs +++ b/src/utils/network/xtream.rs @@ -73,27 +73,27 @@ where if cluster == XtreamCluster::Series { if let Some(content) = xtream_repository::xtream_load_series_info(config, target.name.as_str(), pli.get_virtual_id()) { // Deliver existing target content - return rewrite_xtream_series_info_content(config, target, xtream_output, pli, user, &content); + return rewrite_xtream_series_info_content(config, target, xtream_output, pli, user, &content).await; } // Check if the content has been resolved if xtream_output.resolve_series { if let Some(provider_id) = pli.get_provider_id() { if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Series) { - return xtream_repository::write_and_get_xtream_series_info(config, target, xtream_output, pli, user, &content); + return xtream_repository::write_and_get_xtream_series_info(config, target, xtream_output, pli, user, &content).await; } } } } else if cluster == XtreamCluster::Video { if let Some(content) = xtream_repository::xtream_load_vod_info(config, target.name.as_str(), pli.get_virtual_id()) { // Deliver existing target content - return rewrite_xtream_vod_info_content(config, xtream_output, pli, user, &content); + return rewrite_xtream_vod_info_content(config, xtream_output, pli, user, &content).await; } // Check if the content has been resolved if xtream_output.resolve_vod { if let Some(provider_id) = pli.get_provider_id() { if let Some(content) = xtream_get_input_info(config, input, provider_id, XtreamCluster::Video) { - return xtream_repository::write_and_get_xtream_vod_info(config, target, xtream_output, pli, user, &content); + return xtream_repository::write_and_get_xtream_vod_info(config, target, xtream_output, pli, user, &content).await; } } } @@ -102,8 +102,8 @@ where if let Ok(content) = get_xtream_stream_info_content(client, info_url, input).await { return match cluster { XtreamCluster::Live => Ok(content), - XtreamCluster::Video => xtream_repository::write_and_get_xtream_vod_info(config, target, xtream_output, pli, user, &content), - XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, xtream_output, pli, user, &content), + XtreamCluster::Video => xtream_repository::write_and_get_xtream_vod_info(config, target, xtream_output, pli, user, &content).await, + XtreamCluster::Series => xtream_repository::write_and_get_xtream_series_info(config, target, xtream_output, pli, user, &content).await, }; }