Merge pull request #315 from euzu/feature/mapper_script_view

Feature/mapper script view
This commit is contained in:
euzu
2025-08-08 12:07:12 +02:00
committed by GitHub
403 changed files with 1266 additions and 12132 deletions
+1 -1
View File
@@ -11,4 +11,4 @@ docker/
frontend/node_modules/
frontend/build/
webui/dist
frontend/dist
+1
View File
@@ -4,6 +4,7 @@
- Added hanging client connection release
- Added `replace` built-in function for mapper scripts
- New WebUI features
- Added `token_ttl_mins` to web_auth config to define auth token expiration duration.
# 3.1.4 (2025-06-17)
- share live stream refactored
+1 -1
View File
@@ -1,5 +1,5 @@
[workspace]
members = ["backend", "webui", "shared"]
members = ["backend", "frontend", "shared"]
resolver = "2"
[profile.release]
+1
View File
@@ -320,6 +320,7 @@ log:
- `enabled` can be deactivated if `enabled` is set to `false`. If not set default is `true`.
- `issuer`
- `secret` is used for jwt token generation.
- `token_ttl_mins` default 30 minutes, setting it to 0 uses a 100-year expiration (effectively no expiration)—not recommended for production. !!CAUTION SECURITY RISK!!!
- `userfile` is the file where the ui users are stored. If the filename is not absolute, `tuliprox` will look into the `config_dir`. If `userfile` is not given, the default value is `user.txt`.
```yaml
web_ui:
+1 -1
View File
@@ -103,7 +103,7 @@ impl FromStr for ApiStreamContext {
Self::TIMESHIFT => Ok(Self::Timeshift),
_ => create_tuliprox_error_result!(
TuliproxErrorKind::Info,
"Unknown CounterModifier: {}",
"Unknown ApiStreamContext: {}",
s
),
}
@@ -1,4 +1,4 @@
use crate::api::model::{ProviderConfig, ProviderConfigConnection, ProviderConfigWrapper, ProviderConnectionChangeSender};
use crate::api::model::{ProviderConfig, ProviderConfigConnection, ProviderConfigWrapper, ProviderConnectionChangeCallback, ProviderConnectionChangeSender};
use crate::model::{AppConfig, ConfigInput};
use arc_swap::ArcSwap;
use log::{debug, log_enabled, trace};
@@ -194,12 +194,12 @@ struct SingleProviderLineup {
}
impl SingleProviderLineup {
fn new<'a, F>(cfg: &ConfigInput, get_connection: Option<F>, connection_change_sender: ProviderConnectionChangeSender) -> Self
fn new<'a, F>(cfg: &ConfigInput, get_connection: Option<F>, connection_change: &ProviderConnectionChangeCallback) -> Self
where
F: Fn(&str) -> Option<&'a ProviderConfigConnection>,
{
Self {
provider: ProviderConfigWrapper::new(ProviderConfig::new(cfg, get_connection, connection_change_sender)),
provider: ProviderConfigWrapper::new(ProviderConfig::new(cfg, get_connection, Arc::clone(connection_change))),
}
}
@@ -264,19 +264,20 @@ impl ProviderPriorityGroup {
#[repr(align(64))]
#[derive(Debug)]
struct MultiProviderLineup {
name: String,
providers: Vec<ProviderPriorityGroup>,
index: AtomicUsize,
}
impl MultiProviderLineup {
pub fn new<'a, F>(input: &ConfigInput, get_connection: Option<F>, connection_change_sender: &ProviderConnectionChangeSender) -> Self
pub fn new<'a, F>(cfg_input: &ConfigInput, get_connection: Option<F>, connection_change: &ProviderConnectionChangeCallback) -> Self
where
F: Fn(&str) -> Option<&'a ProviderConfigConnection> + Copy,
{
let mut inputs = vec![ProviderConfigWrapper::new(ProviderConfig::new(input, get_connection, connection_change_sender.clone()))];
if let Some(aliases) = &input.aliases {
let mut inputs = vec![ProviderConfigWrapper::new(ProviderConfig::new(cfg_input, get_connection, Arc::clone(connection_change)))];
if let Some(aliases) = &cfg_input.aliases {
for alias in aliases {
inputs.push(ProviderConfigWrapper::new(ProviderConfig::new_alias(input, alias, get_connection, connection_change_sender.clone())));
inputs.push(ProviderConfigWrapper::new(ProviderConfig::new_alias(cfg_input, alias, get_connection, Arc::clone(connection_change))));
}
}
let mut providers = HashMap::new();
@@ -297,6 +298,7 @@ impl MultiProviderLineup {
}).collect();
Self {
name: cfg_input.name.clone(),
providers,
index: AtomicUsize::new(0),
}
@@ -484,6 +486,24 @@ impl MultiProviderLineup {
}
}
}
pub async fn get_total_connections(&self) -> usize {
let mut total_connections = 0;
for group in &self.providers {
match group {
ProviderPriorityGroup::SingleProviderGroup(provider) => {
total_connections += provider.get_current_connections().await;
},
ProviderPriorityGroup::MultiProviderGroup(_, providers) => {
for provider in providers {
total_connections += provider.get_current_connections().await;
}
}
}
}
total_connections
}
}
@@ -507,13 +527,24 @@ impl ProviderLineupManager {
}
}
fn create_lineup(input: &ConfigInput, provider_connections: Option<&HashMap<&str, ProviderConfigConnection>>, connection_change_sender: ProviderConnectionChangeSender) -> ProviderLineup {
fn create_lineup(cfg_input: &ConfigInput, provider_connections: Option<&HashMap<&str, ProviderConfigConnection>>, connection_change_sender: ProviderConnectionChangeSender) -> ProviderLineup {
let get_connections = provider_connections.map(|c| |name: &str| c.get(name));
if input.aliases.as_ref().is_some_and(|a| !a.is_empty()) {
ProviderLineup::Multi(MultiProviderLineup::new(input, get_connections, &connection_change_sender))
let cfg_name = cfg_input.name.clone();
let on_connection_change: ProviderConnectionChangeCallback = Arc::new(move |_name: &str, connections: usize| {
let connection_change_sender = connection_change_sender.clone();
let cfg_name = cfg_name.clone();
tokio::spawn(async move {
let _ = connection_change_sender.send((cfg_name, connections)).await;
});
});
let on_connection_change = Arc::new(on_connection_change);
if cfg_input.aliases.as_ref().is_some_and(|a| !a.is_empty()) {
ProviderLineup::Multi(MultiProviderLineup::new(cfg_input, get_connections, &on_connection_change))
} else {
ProviderLineup::Single(SingleProviderLineup::new(input, get_connections, connection_change_sender))
ProviderLineup::Single(SingleProviderLineup::new(cfg_input, get_connections, &on_connection_change))
}
}
@@ -711,47 +742,21 @@ impl ProviderLineupManager {
pub async fn active_connections(&self) -> Option<HashMap<String, usize>> {
let mut result = HashMap::<String, usize>::new();
let mut add_provider = async |provider: Option<&ProviderConfig>, name: Option<String>, count: usize| {
if let Some(provider_cfg) = provider {
let count = provider_cfg.get_current_connections().await;
if count > 0 {
result.insert(provider_cfg.name.to_string(), count);
}
} else if count > 0 {
result.insert(name.unwrap_or_default(), count);
let mut add_provider = |name: String, count: usize| {
if count > 0 {
result.insert(name, count);
}
};
let providers = self.providers.load();
for lineup in providers.iter() {
match lineup {
ProviderLineup::Single(provider_lineup) => {
add_provider(Some(&provider_lineup.provider), None, 0).await;
let connections = provider_lineup.provider.get_current_connections().await;
add_provider(provider_lineup.provider.name.clone(), connections);
}
ProviderLineup::Multi(provider_lineup) => {
for provider_group in &provider_lineup.providers {
match provider_group {
ProviderPriorityGroup::SingleProviderGroup(provider) => {
add_provider(Some(provider), None, 0).await;
}
ProviderPriorityGroup::MultiProviderGroup(_, providers) => {
let mut connections = 0;
let mut name = None;
for provider in providers {
if name.is_none() {
name = Some(format!("[{}]", provider.name.clone()));
}
connections += provider.get_current_connections().await;
}
if connections > 0 {
add_provider(None, name, connections).await;
}
// for provider in providers {
// add_provider(provider).await;
// }
}
}
}
let connections = provider_lineup.get_total_connections().await;
add_provider(provider_lineup.name.clone(), connections);
}
}
}
@@ -1042,9 +1047,10 @@ mod tests {
#[test]
fn test_acquire_when_capacity_available() {
let cfg = create_config_input(1, "provider5_1", 1, 2);
let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1);
let on_connection_change = move |_name, _connections| {};
let change_tx = Arc::new(on_connection_change);
let dummy_get_connection = |_s: &str| -> Option<&ProviderConfigConnection> { None };
let lineup = SingleProviderLineup::new(&cfg, Some(dummy_get_connection), change_tx);
let lineup = SingleProviderLineup::new(&cfg, Some(dummy_get_connection), &change_tx);
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
// First acquire attempt should succeed
@@ -1063,10 +1069,11 @@ mod tests {
#[test]
fn test_release_connection() {
let cfg = create_config_input(1, "provider7_1", 1, 2);
let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1);
let on_connection_change = move |_name, _connections| {};
let change_tx = Arc::new(on_connection_change);
let dummy_get_connection = |_s: &str| -> Option<&ProviderConfigConnection> { None };
let lineup = SingleProviderLineup::new(&cfg, Some(dummy_get_connection), change_tx);
let lineup = SingleProviderLineup::new(&cfg, Some(dummy_get_connection), &change_tx);
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async move {
// Acquire two connections
@@ -1127,9 +1134,10 @@ mod tests {
#[test]
fn test_concurrent_acquire() {
let cfg = create_config_input(1, "provider9_1", 1, 2);
let (change_tx, _) = tokio::sync::mpsc::channel::<(String, usize)>(1);
let on_connection_change = move |_name, _connections| {};
let change_tx = Arc::new(on_connection_change);
let dummy_get_connection = |_s: &str| -> Option<&ProviderConfigConnection> { None };
let lineup = Arc::new(SingleProviderLineup::new(&cfg, Some(dummy_get_connection), change_tx));
let lineup = Arc::new(SingleProviderLineup::new(&cfg, Some(dummy_get_connection), &change_tx));
let available_count = Arc::new(AtomicU16::new(2));
let grace_period_count = Arc::new(AtomicU16::new(1));
+26 -12
View File
@@ -12,6 +12,8 @@ use shared::write_if_some;
pub type ProviderConnectionChangeSender = tokio::sync::mpsc::Sender<(String, usize)>;
pub type ProviderConnectionChangeReceiver = tokio::sync::mpsc::Receiver<(String, usize)>;
pub type ProviderConnectionChangeCallback = Arc<dyn Fn(&str, usize) + Send + Sync>;
#[derive(Debug, Clone, Copy)]
pub enum ProviderConfigAllocation {
Exhausted,
@@ -33,7 +35,6 @@ pub struct ProviderConfigConnection {
/// `max_connections`: Maximum allowed concurrent connections.
/// `priority`: Priority level for selecting providers.
/// `current_connections`: A `RwLock` to safely track the number of active connections.
#[derive(Debug)]
pub struct ProviderConfig {
pub id: u16,
pub name: String,
@@ -44,7 +45,8 @@ pub struct ProviderConfig {
max_connections: usize,
priority: i16,
connection: RwLock<ProviderConfigConnection>,
connection_change_tx: ProviderConnectionChangeSender,
//connection_change_tx: ProviderConnectionChangeSender,
on_connection_change: ProviderConnectionChangeCallback,
}
impl fmt::Display for ProviderConfig {
@@ -65,6 +67,12 @@ impl fmt::Display for ProviderConfig {
}
}
impl fmt::Debug for ProviderConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl PartialEq for ProviderConfig {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
@@ -81,17 +89,23 @@ impl PartialEq for ProviderConfig {
macro_rules! modify_connections {
($self:ident, $guard:ident, +1) => {{
$guard.current_connections += 1;
$self.notify_connection_change($guard.current_connections).await;
let cnt = {
$guard.current_connections += 1;
$guard.current_connections
};
$self.notify_connection_change(cnt);
}};
($self:ident, $guard:ident, -1) => {{
$guard.current_connections -= 1;
$self.notify_connection_change($guard.current_connections).await;
let cnt = {
$guard.current_connections -= 1;
$guard.current_connections
};
$self.notify_connection_change(cnt);
}};
}
impl ProviderConfig {
pub fn new<'a, F>(cfg: &ConfigInput, get_connection: Option<F>, connection_change_tx: ProviderConnectionChangeSender) -> Self
pub fn new<'a, F>(cfg: &ConfigInput, get_connection: Option<F>, on_connection_change: ProviderConnectionChangeCallback) -> Self
where
F: Fn(&str) -> Option<&'a ProviderConfigConnection>,
{
@@ -105,11 +119,11 @@ impl ProviderConfig {
max_connections: cfg.max_connections as usize,
priority: cfg.priority,
connection: RwLock::new(get_connection.and_then(|f| f(cfg.name.as_str())).map_or_else(Default::default, Clone::clone)),
connection_change_tx
on_connection_change
}
}
pub fn new_alias<'a, F>(cfg: &ConfigInput, alias: &ConfigInputAlias, get_connection: Option<F>, connection_change_tx: ProviderConnectionChangeSender) -> Self
pub fn new_alias<'a, F>(cfg: &ConfigInput, alias: &ConfigInputAlias, get_connection: Option<F>, on_connection_change: ProviderConnectionChangeCallback) -> Self
where
F: Fn(&str) -> Option<&'a ProviderConfigConnection>,
{
@@ -123,7 +137,7 @@ impl ProviderConfig {
max_connections: alias.max_connections as usize,
priority: alias.priority,
connection: RwLock::new(get_connection.and_then(|f| f(alias.name.as_str())).map_or_else(Default::default, Clone::clone)),
connection_change_tx,
on_connection_change,
}
}
@@ -131,8 +145,8 @@ impl ProviderConfig {
InputUserInfo::new(self.input_type, self.username.as_deref(), self.password.as_deref(), &self.url)
}
async fn notify_connection_change(&self, new_connections: usize) {
let _ = self.connection_change_tx.send((self.name.clone(), new_connections)).await;
fn notify_connection_change(&self, new_connections: usize) {
(self.on_connection_change)(&self.name, new_connections);
}
#[inline]
+6 -1
View File
@@ -32,7 +32,12 @@ fn create_jwt(web_auth_config: &WebAuthConfig, username: &str, roles: Vec<String
header.typ = Some("JWT".to_string());
let now = Local::now();
let iat = now.timestamp();
let exp = (now + Duration::minutes(30)).timestamp();
let duration = web_auth_config.token_ttl_mins;
let exp = if duration > 0 {
(now + Duration::minutes(i64::from(duration))).timestamp()
} else {
(now + Duration::days(365 * 100)).timestamp() // 100 years
};
let claims = Claims {
username: username.to_string(),
iss: web_auth_config.issuer.clone(),
+7 -4
View File
@@ -11,6 +11,7 @@ pub struct WebAuthConfig {
pub enabled: bool,
pub issuer: String,
pub secret: String,
pub token_ttl_mins: u32,
pub userfile: Option<String>,
pub t_users: Option<Vec<UserCredential>>,
}
@@ -20,8 +21,9 @@ impl From<&WebAuthConfigDto> for WebAuthConfig {
fn from(dto: &WebAuthConfigDto) -> Self {
Self {
enabled: dto.enabled,
issuer: dto.issuer.to_string(),
secret: dto.secret.to_string(),
issuer: dto.issuer.clone(),
secret: dto.secret.clone(),
token_ttl_mins: dto.token_ttl_mins,
userfile: dto.userfile.clone(),
t_users: None,
}
@@ -32,8 +34,9 @@ impl From<&WebAuthConfig> for WebAuthConfigDto {
fn from(instance: &WebAuthConfig) -> Self {
Self {
enabled: instance.enabled,
issuer: instance.issuer.to_string(),
secret: instance.secret.to_string(),
issuer: instance.issuer.clone(),
secret: instance.secret.clone(),
token_ttl_mins: instance.token_ttl_mins,
userfile: instance.userfile.clone(),
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
echo "building binary for aarch64"
env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target aarch64-unknown-linux-musl
cd ./webui || exit 1
cd ./frontend || exit 1
env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
echo "building binary for armv7"
env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target armv7-unknown-linux-musleabihf
cd ./webui || exit 1
cd ./frontend || exit 1
env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target x86_64-apple-darwin
cd ./webui || exit 1
cd ./frontend || exit 1
env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
+1 -1
View File
@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cd ./webui && rm -rf dist && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
cd ./frontend && rm -rf dist && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
+1 -1
View File
@@ -7,7 +7,7 @@ BIN_DIR="${WORKING_DIR}/bin"
RESOURCES_DIR="${WORKING_DIR}/resources"
DOCKER_DIR="${WORKING_DIR}/docker"
BACKEND_DIR="${WORKING_DIR}/backend"
FRONTEND_DIR="${WORKING_DIR}/webui"
FRONTEND_DIR="${WORKING_DIR}/frontend"
FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist"
declare -A ARCHITECTURES=(
[LINUX]=x86_64-unknown-linux-musl
+1 -1
View File
@@ -28,7 +28,7 @@ BIN_DIR="${WORKING_DIR}/bin"
RESOURCES_DIR="${WORKING_DIR}/resources"
DOCKER_DIR="${WORKING_DIR}/docker"
BACKEND_DIR="${WORKING_DIR}/backend"
FRONTEND_DIR="${WORKING_DIR}/webui"
FRONTEND_DIR="${WORKING_DIR}/frontend"
FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist"
TARGET=aarch64-unknown-linux-musl
VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml" || true)
+1 -1
View File
@@ -7,7 +7,7 @@ BIN_DIR="${WORKING_DIR}/bin"
RESOURCES_DIR="${WORKING_DIR}/resources"
DOCKER_DIR="${WORKING_DIR}/docker"
BACKEND_DIR="${WORKING_DIR}/backend"
FRONTEND_DIR="${WORKING_DIR}/webui"
FRONTEND_DIR="${WORKING_DIR}/frontend"
FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist"
TARGET=x86_64-unknown-linux-musl
VERSION=$(grep -Po '^version\s*=\s*"\K[0-9\.]+' "${BACKEND_DIR}/Cargo.toml" || true)
+1 -1
View File
@@ -5,4 +5,4 @@ if [ "$(uname)" != "Linux" ]; then
else
cargo build -p tuliprox --release
fi
cd "./webui" && trunk build --release
cd "./frontend" && trunk build --release
+1 -1
View File
@@ -2,4 +2,4 @@
DEFAULT_TARGET="x86_64-unknown-linux-musl"
TARGET="${1:-$DEFAULT_TARGET}"
env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target "$TARGET"
cd "./webui" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
cd "./frontend" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
+1 -1
View File
@@ -3,4 +3,4 @@ env RUSTFLAGS="--remap-path-prefix $HOME=~" cargo build -p tuliprox --release --
# cross build, is only working with cargo clean
# env RUSTFLAGS="--remap-path-prefix $HOME=~" cross build -p tuliprox --release --target x86_64-pc-windows-gnu
cd "./webui" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
cd "./frontend" && env RUSTFLAGS="--remap-path-prefix $HOME=~" trunk build --release
+1 -1
View File
@@ -5,7 +5,7 @@ set -o pipefail
WORKING_DIR=$(pwd)
RESOURCES_DIR="$WORKING_DIR/resources"
RELEASE_DIR="$WORKING_DIR/release"
FRONTEND_DIR="${WORKING_DIR}/webui"
FRONTEND_DIR="${WORKING_DIR}/frontend"
FRONTEND_BUILD_DIR="${FRONTEND_DIR}/dist"
if ! command -v cargo-set-version &> /dev/null
+4 -4
View File
@@ -58,7 +58,7 @@ RUN cargo install --locked trunk wasm-bindgen-cli
# Now copy the actual source code and build the project
COPY . .
WORKDIR /src/webui
WORKDIR /src/frontend
# Run Trunk build
RUN trunk build --release
@@ -135,7 +135,7 @@ RUN rustup update && rustup target add $RUST_TARGET
# Create the final application layout to mimic production
WORKDIR /app
COPY --from=trunk-build /src/webui/dist ./web
COPY --from=trunk-build /src/frontend/dist ./web
COPY --from=resource-build /src/resources ./resources
# Create a separate directory for the source code
@@ -170,7 +170,7 @@ COPY --from=rust-build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# App
WORKDIR /app
COPY --from=rust-build /src/target/${RUST_TARGET}/release/tuliprox ./tuliprox
COPY --from=trunk-build /src/webui/dist ./web
COPY --from=trunk-build /src/frontend/dist ./web
COPY --from=resource-build /src/resources ./resources
ENTRYPOINT ["/app/tuliprox"]
@@ -195,7 +195,7 @@ RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime
WORKDIR /app
COPY --from=rust-build /src/target/${RUST_TARGET}/release/tuliprox ./tuliprox
COPY --from=trunk-build /src/webui/dist ./web
COPY --from=trunk-build /src/frontend/dist ./web
COPY --from=resource-build /src/resources ./resources
ENTRYPOINT ["/sbin/tini", "--", "/app/tuliprox"]
-54
View File
@@ -1,54 +0,0 @@
{
"env": {
"jest/globals": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"react-app",
"react-app/jest",
"eslint-config-prettier",
"plugin:react/recommended"
],
"settings": {
"react": {
"createClass": "createReactClass",
"pragma": "React",
"fragment": "Fragment",
"version": "detect",
"flowVersion": "detect"
}
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"import",
"node",
"react"
],
"rules": {
"consistent-this": [
"error",
"self"
],
"@typescript-eslint/no-this-alias": [
"error",
{
"allowDestructuring": false,
"allowedNames": [
"self"
]
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"react/display-name": "off",
"react/prop-types": "off"
},
"ignorePatterns": [ "*.test.tsc", "*.scss", "*.css" ]
}
-83
View File
@@ -1,83 +0,0 @@
{
"name": "tuliprox",
"description": "tuliprox",
"author": "euzu",
"version": "3.1.4",
"private": true,
"homepage": "",
"license": "MIT",
"scripts": {
"react-start": "react-scripts start",
"react-build": "react-scripts build",
"react-test": "react-scripts test --env=jsdom",
"react-eject": "react-scripts eject",
"build": "cross-env GENERATE_SOURCEMAP=false REACT_APP_STAGE=production PUBLIC_URL= yarn react-build",
"start": "yarn react-start",
"lint": "eslint --config eslintrc.json src/** --no-error-on-unmatched-pattern --quiet",
"upgr": "yarn upgrade-interactive --latest",
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
"dependencies": {
"file-saver": "2.0.5",
"i18next": "25.0.1",
"jwt-decode": "4.0.0",
"mpegts.js": "1.8.0",
"notistack": "3.0.2",
"react": "19.1.0",
"react-datepicker": "8.3.0",
"react-dom": "19.1.0",
"react-i18next": "15.4.1",
"react-tag-input-component": "2.0.2",
"rxjs": "7.8.2",
"video.js": "8.22.0",
"videojs-mpegtsjs": "1.0.0"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
"@babel/runtime": "7.27.0",
"@types/file-saver": "2.0.7",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"@types/video.js": "7.3.58",
"@typescript-eslint/eslint-plugin": "8.30.1",
"@typescript-eslint/parser": "8.30.1",
"babel-eslint": "10.1.0",
"concurrently": "9.1.2",
"cross-env": "7.0.3",
"eslint": "9.25.0",
"eslint-config-prettier": "10.0.1",
"eslint-config-react-app": "7.0.1",
"eslint-plugin-flowtype": "8.0.3",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-jest": "28.11.0",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "5.2.0",
"prettier": "3.5.3",
"react-scripts": "5.0.1",
"sass": "1.86.3",
"source-map-explorer": "2.5.3",
"typescript": "5.8.3",
"wait-on": "8.0.3"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"proxy": "http://localhost:8901"
}
+1 -1
View File
@@ -56,4 +56,4 @@
font-weight: 400;
src: url('fonts/rubik-latin-400-normal.woff2') format('woff2'), url('fonts/rubik-latin-400-normal.woff') format('woff');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
}
-123
View File
@@ -1,123 +0,0 @@
{
"LABEL": {
"LIVE": "Live",
"VOD": "VOD",
"SERIES": "Serien",
"SAVE": "Speichern",
"SUBMIT": "Übernehmen",
"OK": "Ok",
"CANCEL": "Abbrechen",
"SHOW_SELECTED": "Ausgewählte anzeigen",
"SELECT_ALL": "Alle auswählen",
"DESELECT_ALL": "Alle abwählen",
"SEARCH": "Suche",
"CLEAR": "Löschen",
"REGEXP": "RegExp",
"LOGOUT": "Abmelden",
"CONFIGURATION": "Konfiguration",
"HOST": "Host",
"PORT": "Port",
"WEB_ROOT": "Web-Root",
"THREADS": "Threads",
"WORKING_DIR": "Arbeitsverzeichnis",
"BACKUP_DIR": "Backup-Verzeichnis",
"UPDATE_ON_BOOT": "Update beim Start",
"WEB_UI": "Web-UI",
"SCHEDULES": "Zeitpläne",
"SANITIZE_SENSITIVE_INFO": "Sensiblen Inhalt bereinigen",
"ACTIVE_CLIENTS": "Aktive Clients",
"RESOURCE_REWRITE_DISABLE": "Ressourcenumschreibung deaktivieren",
"RECONNECT": "Erneut verbinden",
"BUFFER_ENABLED": "Puffer aktiviert",
"BUFFER_SIZE": "Puffergröße",
"ENABLED": "Aktiviert",
"SIZE": "Größe",
"CACHE_DIR": "Cache-Verzeichnis",
"NOTIFY_ON": "Benachrichtigen bei",
"BOT_TOKEN": "Bot-Token",
"CHAT_IDS": "Chat-IDs",
"URL": "URL",
"USER": "Benutzer",
"USERS": "Benutzer",
"TOKEN": "Token",
"WEB_SEARCH_URL": "Web-Such-URL",
"EXTENSIONS": "Erweiterungen",
"HEADERS": "Header",
"DIRECTORY": "Verzeichnis",
"ORGANIZE_INTO_DIRECTORIES": "In Verzeichnisse organisieren",
"EPISODE_PATTERN": "Episoden-Muster",
"API": "API",
"MAIN": "Haupt",
"REVERSE_PROXY": "ReverseProxy",
"MESSAGING": "Nachrichten",
"VIDEO": "Video",
"EXAMPLES": "Beispiele",
"UPDATE": "Aktualisieren",
"START": "Starten",
"CACHE": "Cache",
"STREAM": "Stream",
"LOG": "Protokoll",
"CONFIG": "Konfiguration",
"TELEGRAM": "Telegram",
"REST": "Rest",
"PUSHOVER": "Pushover",
"PROXY": "Proxy",
"REFRESH": "Aktualisieren",
"STATUS": "Status",
"MAX_CON": "MaxCon",
"EXP_DATE": "Ablaufdatum",
"USERNAME": "Benutzername",
"PASSWORD": "Passwort",
"SERVER": "Server",
"TIMEZONE": "Zeitzone",
"PATH": "Pfad",
"MESSAGE": "Nachricht",
"PROTOCOL": "Protokoll",
"NAME": "Name"
},
"TITLE": {
"USER_BOUQUET_EDITOR": "Benutzer-Gruppen-Editor"
},
"MESSAGES": {
"INVALID_REGEXP": "Ungültiger regulärer Ausdruck",
"INVALID_FILETYPE": "Ungültiger Dateityp",
"DOWNLOAD": {
"SUCCESS": "Erfolgreich heruntergeladen",
"FAIL": "Fehler beim Herunterladen!",
"PLAYLIST": {
"SUCCESS": "Playlist erfolgreich heruntergeladen",
"FAIL": "Fehler beim Herunterladen der Playlist!"
},
"SERVER_CONFIG": {
"FAIL": "Fehler beim Herunterladen der Serverkonfiguration!"
}
},
"SAVE": {
"MAIN_CONFIG": {
"SUCCESS": "Hauptkonfiguration gespeichert!",
"FAIL": "Fehler beim Speichern der Hauptkonfiguration!"
},
"BOUQUET": {
"SUCCESS": "Bouquet erfolgreich gespeichert",
"FAIL": "Fehler beim Speichern des Bouquets!"
},
"USER": {
"SUCCESS": "Benutzer erfolgreich gespeichert",
"FAIL": "Fehler beim Speichern des Benutzers!"
},
"PLAYLIST_UPDATE": {
"SUCCESS": "Playlist Aktualisierung erfolgreich gestartet!",
"FAIL": "Playlist Aktualisierung fehlgeschlagen!"
},
"SOURCE_SELECTOR": {
"MISSING_SELECTION": "Kein Eintrag ausgewählt"
}
}
},
"INFO": {
"RESTART_TO_APPLY_CHANGES": "Sie müssen neu starten, um Änderungen anzuwenden.",
"SCHEDULE_EXAMPLE": "Beispiel für Zeitplan",
"THREADS": "Threads: Lassen Sie es auf 0, wenn keine Probleme bestehen. Erhöhen Sie es, wenn Sie mehrere Anbieter haben. Der parallele Zugriff auf Ihren Anbieter kann zu einer Sperrung führen.",
"WEB_ROOT": "Web-Root: Enthält die Web-UI-Dateien. Normalerweise befinden sie sich im gleichen Verzeichnis unter ./web"
}
}
@@ -195,7 +195,10 @@
"ALIAS": "Alias",
"INPUTS": "Inputs",
"NEW_USER": "New User",
"USERLIST": "Userlist"
"USERLIST": "Userlist",
"MAPPER": "Mapper",
"SCRIPT": "Script",
"COUNTER": "Counter"
},
"TABLE": {
"EMPTY": "",
-206
View File
@@ -1,206 +0,0 @@
{
"LABEL": {
"LIVE": "Live",
"VOD": "VOD",
"SERIES": "Series",
"SAVE": "Save",
"SUBMIT": "Submit",
"OK": "Ok",
"CANCEL": "Cancel",
"CLOSE": "Close",
"SHOW_SELECTED": "Show selected",
"SELECT_ALL": "Select All",
"DESELECT_ALL": "Deselect all",
"SEARCH": "Search",
"CLEAR": "Clear",
"REGEXP": "Regexp",
"LOGOUT": "Logout",
"CONFIGURATION": "Configuration",
"HOST": "Host",
"PORT": "Port",
"WEB_ROOT": "Web-Root",
"THREADS": "Threads",
"WORKING_DIR": "Working dir",
"BACKUP_DIR": "Backup dir",
"UPDATE_ON_BOOT": "Update on boot",
"WEB_UI": "Web UI",
"USER_UI": "User UI",
"WEB_UI_PATH": "Path prefix",
"SCHEDULES": "Schedules",
"SANITIZE_SENSITIVE_INFO": "Sanitize sensitive info",
"ACTIVE_USER": "Active User",
"RESOURCE_REWRITE_DISABLE": "Resource Rewrite disable",
"DISABLE_REFERER_HEADER": "Referer header disable",
"RECONNECT": "Reconnect",
"THROTTLE": "Throttle",
"GRACE_PERIOD": "Grace period ms",
"GRACE_PERIOD_TIMEOUT": "Grace period timeout sec",
"BUFFER_ENABLED": "Buffer Enabled",
"BUFFER_SIZE": "Buffer Size",
"ENABLED": "Enabled",
"SIZE": "Size",
"CACHE_DIR": "Cache dir",
"NOTIFY_ON": "Notify on",
"BOT_TOKEN": "Bot Token",
"CHAT_IDS": "Chat Ids",
"URL": "Url",
"USER": "User",
"USERS": "Users",
"TOKEN": "Token",
"WEB_SEARCH_URL": "Web Search Url",
"EXTENSIONS": "Extensions",
"HEADERS": "Headers",
"DIRECTORY": "Directory",
"ORGANIZE_INTO_DIRECTORIES": "Organize Into Directorie",
"EPISODE_PATTERN": "Episode Pattern",
"API": "Api",
"MAIN": "Main",
"REVERSE_PROXY": "ReverseProxy",
"MESSAGING": "Messaging",
"VIDEO": "Video",
"EXAMPLES": "Examples",
"UPDATE": "Update",
"START": "Start",
"CACHE": "Cache",
"STREAM": "Stream",
"LOG": "Log",
"LOG_LEVEL": "Log level",
"CONFIG": "Config",
"TELEGRAM": "Telegram",
"REST": "Rest",
"PUSHOVER": "Pushover",
"PROXY": "Proxy",
"REFRESH": "Refresh",
"STATUS": "Status",
"MAX_CON": "MaxCon",
"EXP_DATE": "ExpDate",
"USERNAME": "Username",
"PASSWORD": "Password",
"SERVER": "Server",
"TIMEZONE": "Timezone",
"PATH": "Path",
"MESSAGE": "Message",
"PROTOCOL": "Protocol",
"NAME": "Name",
"API_PROXY": "Api-Proxy",
"INFINITE": "Infinite",
"ONE_YEAR": "One year",
"SELF_HOSTED": "Self Hosted",
"PROVIDER": "Provider",
"CUSTOM": "Custom",
"XTREAM_CODES": "xtream codes",
"XC": "xc",
"M3U": "m3u",
"PLAYLIST_BROWSER": "Playlist Browser",
"RATE_LIMIT": "Rate Limit",
"PERIOD_MILLIS": "Period Millis",
"BURST_SIZE": "Burst Size",
"WEB_AUTH": "Auth",
"ISSUER": "Issuer",
"SECRET": "Secret",
"USERFILE": "Userfile",
"UI_ENABLED": "UI",
"PLAYER_SERVER": "Player Server",
"VERSION": "Version",
"SERVER_TIME": "Server time",
"BUILD_TIME": "Build time",
"MEMORY": "Memory",
"ACTIVE_USERS": "Active users",
"ACTIVE_USER_CONNECTIONS": "Active user conn...",
"ACTIVE_PROVIDER_CONNECTIONS": "Active provider conn...",
"IP_CHECK": "IP Check",
"URL_IPV4": "Url IPv4",
"URL_IPV6": "Url IPv6",
"PATTERN_IPV4": "Pattern IPv4",
"PATTERN_IPV6": "Pattern IPv6",
"IPv4": "IPv4",
"IPv6": "IPv6",
"IP": "IP",
"TARGET": "Target",
"TARGETS": "Targets",
"NOTES": "Notes"
},
"TITLE": {
"USER_BOUQUET_EDITOR": "User group editor"
},
"MESSAGES": {
"NO_CONTENT": "No content",
"INVALID_REGEXP": "Invalid regexp",
"INVALID_FILETYPE": "Invalid filetype",
"TARGET_NOT_EXISTS": "Target does not exist",
"DOWNLOAD": {
"SUCCESS": "Successfully downloaded",
"FAIL": "Failed to download!",
"PLAYLIST": {
"SUCCESS": "Successfully downloaded playlist",
"FAIL": "Failed to download playlist!"
},
"SERVER_CONFIG": {
"FAIL": "Failed to download server config!"
}
},
"SAVE": {
"MAIN_CONFIG": {
"SUCCESS": "Main config saved!",
"FAIL": "Failed to save main config!"
},
"BOUQUET": {
"SUCCESS": "Successfully saved bouquet",
"FAIL": "Failed to save bouquet!"
},
"USER": {
"SUCCESS": "Successfully saved user",
"FAIL": "Failed to save user!",
"USERNAME_REQUIRED": "Username required",
"DUPLICATE_USERNAME": "Duplicate username!",
"DUPLICATE_TOKEN": "Duplicate token!",
"TARGET_NOT_FOUND": "Target not found!",
"MAX_CONNECTIONS_INVALID": "MaxConnections invalid"
}
},
"PLAYLIST_UPDATE": {
"SUCCESS": "Successfully started playlist update!",
"FAIL": "Playlist update failed!"
},
"SOURCE_SELECTOR": {
"MISSING_SELECTION": "No entry selected"
}
},
"INFO": {
"RESTART_TO_APPLY_CHANGES": "You need to restart to apply changes.",
"SCHEDULE_EXAMPLE": "Schedule example",
"THREADS": "Threads: keep it 0 if you have no problems. Increase it if you have multiple providers. Accessing you provider parallel can cause a ban.",
"WEB_ROOT": "Web-Root: Contains the web-ui files. They are normally placed in the same directory under ./web"
},
"HINT": {
"CONFIG": {
"API_PROXY": {
"PATH": "URL path prefix"
},
"USER": {
"TARGET_FORCE_REDIRECT": "Force Redirect is set for target"
},
"LOG": {
"ACTIVE_USER": "Activates active user info log"
},
"PROXY": {
"RESOURCE_REWRITE_DISABLE": "If activated, the URL rewrite of resources in reverse proxy mode is disabled",
"DISABLE_REFERER_HEADER": "If activated, the referer header is not sent to the provider",
"THROTTLE": "Supported units are KB/s,MB/s,KiB/s,MiB/s,kbps,mbps,Mibps"
},
"WEB_UI": {
"PATH": "Configuration of a path prefix, allowing it to be accessed behind a reverse proxy under a custom subpath. like https://yourdomain.com/tuliprox"
}
}
},
"SETUP":{
"LABEL": {
"NEXT": "Next",
"BACK": "Back",
"FINISH_SETUP": "Finish Setup"
},
"MSG": {
"WELCOME": "Welcome to tuliprox setup. This wizard will guide you through the initial setup process."
}
}
}
-112
View File
@@ -1,112 +0,0 @@
{
"LABEL": {
"LIVE": "En vivo",
"VOD": "VOD",
"SERIES": "Series",
"SAVE": "Guardar",
"SHOW_SELECTED": "Mostrar seleccionados",
"SELECT_ALL": "Seleccionar todo",
"DESELECT_ALL": "Deseleccionar todo",
"SEARCH": "Buscar",
"CLEAR": "Borrar",
"REGEXP": "Expresión regular",
"LOGOUT": "Cerrar sesión",
"CONFIGURATION": "Configuración",
"HOST": "Host",
"PORT": "Puerto",
"WEB_ROOT": "Raíz web",
"THREADS": "Hilos",
"WORKING_DIR": "Directorio de trabajo",
"BACKUP_DIR": "Directorio de respaldo",
"UPDATE_ON_BOOT": "Actualizar al arrancar",
"WEB_UI": "Interfaz web",
"SCHEDULES": "Horarios",
"SANITIZE_SENSITIVE_INFO": "Sanitizar información sensible",
"ACTIVE_CLIENTS": "Clientes activos",
"RESOURCE_REWRITE_DISABLE": "Desactivar reescritura de recursos",
"RECONNECT": "Reconectar",
"BUFFER_ENABLED": "Búfer activado",
"BUFFER_SIZE": "Tamaño del búfer",
"ENABLED": "Habilitado",
"SIZE": "Tamaño",
"CACHE_DIR": "Directorio de caché",
"NOTIFY_ON": "Notificar en",
"BOT_TOKEN": "Token del bot",
"CHAT_IDS": "IDs de chat",
"URL": "URL",
"USER": "Usuario",
"TOKEN": "Token",
"WEB_SEARCH_URL": "URL de búsqueda web",
"EXTENSIONS": "Extensiones",
"HEADERS": "Encabezados",
"DIRECTORY": "Directorio",
"ORGANIZE_INTO_DIRECTORIES": "Organizar en directorios",
"EPISODE_PATTERN": "Patrón de episodio",
"API": "API",
"MAIN": "Principal",
"REVERSE_PROXY": "Proxy inverso",
"MESSAGING": "Mensajería",
"VIDEO": "Video",
"EXAMPLES": "Ejemplos",
"UPDATE": "Actualizar",
"START": "Iniciar",
"CACHE": "Caché",
"STREAM": "Flujo",
"LOG": "Registro",
"CONFIG": "Configuración",
"TELEGRAM": "Telegram",
"REST": "Rest",
"PUSHOVER": "Pushover",
"PROXY": "Proxy",
"REFRESH": "Actualizar",
"STATUS": "Estado",
"MAX_CON": "MaxCon",
"EXP_DATE": "Fecha de expiración",
"USERNAME": "Nombre de usuario",
"PASSWORD": "Contraseña",
"SERVER": "Servidor",
"TIMEZONE": "Zona horaria",
"PATH": "Ruta",
"MESSAGE": "Mensaje",
"PROTOCOL": "Protocolo",
"NAME": "Nombre"
},
"TITLE": {
"USER_BOUQUET_EDITOR": "Editor de bouquet de usuario"
},
"MESSAGES": {
"INVALID_REGEXP": "Expresión regular no válida",
"INVALID_FILETYPE": "Tipo de archivo no válido",
"DOWNLOAD": {
"SUCCESS": "Descargado con éxito",
"FAIL": "¡Error al descargar!",
"PLAYLIST": {
"SUCCESS": "Lista de reproducción descargada con éxito",
"FAIL": "¡Error al descargar la lista de reproducción!"
},
"SERVER_CONFIG": {
"FAIL": "¡Error al descargar la configuración del servidor!"
}
},
"SAVE": {
"MAIN_CONFIG": {
"SUCCESS": "¡Configuración principal guardada!",
"FAIL": "¡Error al guardar la configuración principal!"
},
"BOUQUET": {
"SUCCESS": "Bouquet guardado con éxito",
"FAIL": "¡Error al guardar el bouquet!"
},
"USER": {
"SUCCESS": "Usuario guardado con éxito",
"FAIL": "¡Error al guardar el usuario!"
}
}
},
"INFO": {
"RESTART_TO_APPLY_CHANGES": "Necesitas reiniciar para aplicar los cambios.",
"SCHEDULE_EXAMPLE": "Ejemplo de horario",
"THREADS": "Hilos: mantén en 0 si no tienes problemas. Aumenta si tienes varios proveedores. Acceder a tu proveedor en paralelo puede causar un baneo.",
"WEB_ROOT": "Raíz web: contiene los archivos de la interfaz web. Normalmente se colocan en el mismo directorio bajo ./web"
}
}
-112
View File
@@ -1,112 +0,0 @@
{
"LABEL": {
"LIVE": "En direct",
"VOD": "VOD",
"SERIES": "Séries",
"SAVE": "Enregistrer",
"SHOW_SELECTED": "Afficher la sélection",
"SELECT_ALL": "Tout sélectionner",
"DESELECT_ALL": "Tout désélectionner",
"SEARCH": "Rechercher",
"CLEAR": "Effacer",
"REGEXP": "Expression régulière",
"LOGOUT": "Se déconnecter",
"CONFIGURATION": "Configuration",
"HOST": "Hôte",
"PORT": "Port",
"WEB_ROOT": "Racine Web",
"THREADS": "Threads",
"WORKING_DIR": "Répertoire de travail",
"BACKUP_DIR": "Répertoire de sauvegarde",
"UPDATE_ON_BOOT": "Mettre à jour au démarrage",
"WEB_UI": "Interface Web",
"SCHEDULES": "Programmes",
"SANITIZE_SENSITIVE_INFO": "Assainir les informations sensibles",
"ACTIVE_CLIENTS": "Clients actifs",
"RESOURCE_REWRITE_DISABLE": "Désactiver la réécriture des ressources",
"RECONNECT": "Se reconnecter",
"BUFFER_ENABLED": "Mise en mémoire tampon activée",
"BUFFER_SIZE": "Taille du tampon",
"ENABLED": "Activé",
"SIZE": "Taille",
"CACHE_DIR": "Répertoire du cache",
"NOTIFY_ON": "Notifier lorsqu'il y a",
"BOT_TOKEN": "Jeton du bot",
"CHAT_IDS": "Identifiants de chat",
"URL": "URL",
"USER": "Utilisateur",
"TOKEN": "Jeton",
"WEB_SEARCH_URL": "URL de recherche Web",
"EXTENSIONS": "Extensions",
"HEADERS": "En-têtes",
"DIRECTORY": "Répertoire",
"ORGANIZE_INTO_DIRECTORIES": "Organiser dans des répertoires",
"EPISODE_PATTERN": "Modèle d'épisode",
"API": "API",
"MAIN": "Principal",
"REVERSE_PROXY": "Proxy inverse",
"MESSAGING": "Messagerie",
"VIDEO": "Vidéo",
"EXAMPLES": "Exemples",
"UPDATE": "Mettre à jour",
"START": "Démarrer",
"CACHE": "Cache",
"STREAM": "Diffusion",
"LOG": "Journal",
"CONFIG": "Configuration",
"TELEGRAM": "Telegram",
"REST": "Rest",
"PUSHOVER": "Pushover",
"PROXY": "Proxy",
"REFRESH": "Rafraîchir",
"STATUS": "Statut",
"MAX_CON": "MaxCon",
"EXP_DATE": "Date d'expiration",
"USERNAME": "Nom d'utilisateur",
"PASSWORD": "Mot de passe",
"SERVER": "Serveur",
"TIMEZONE": "Fuseau horaire",
"PATH": "Chemin",
"MESSAGE": "Message",
"PROTOCOL": "Protocole",
"NAME": "Nom"
},
"TITLE": {
"USER_BOUQUET_EDITOR": "Éditeur de bouquet utilisateur"
},
"MESSAGES": {
"INVALID_REGEXP": "Expression régulière invalide",
"INVALID_FILETYPE": "Type de fichier invalide",
"DOWNLOAD": {
"SUCCESS": "Téléchargement réussi",
"FAIL": "Échec du téléchargement!",
"PLAYLIST": {
"SUCCESS": "Liste de lecture téléchargée avec succès",
"FAIL": "Échec du téléchargement de la liste de lecture!"
},
"SERVER_CONFIG": {
"FAIL": "Échec du téléchargement de la configuration du serveur!"
}
},
"SAVE": {
"MAIN_CONFIG": {
"SUCCESS": "Configuration principale enregistrée!",
"FAIL": "Échec de l'enregistrement de la configuration principale!"
},
"BOUQUET": {
"SUCCESS": "Bouquet enregistré avec succès",
"FAIL": "Échec de l'enregistrement du bouquet!"
},
"USER": {
"SUCCESS": "Utilisateur enregistré avec succès",
"FAIL": "Échec de l'enregistrement de l'utilisateur!"
}
}
},
"INFO": {
"RESTART_TO_APPLY_CHANGES": "Vous devez redémarrer pour appliquer les modifications.",
"SCHEDULE_EXAMPLE": "Exemple de programme",
"THREADS": "Threads: laissez-le à 0 si vous n'avez aucun problème. Augmentez-le si vous avez plusieurs fournisseurs. L'accès parallèle à votre fournisseur peut entraîner un ban.",
"WEB_ROOT": "Racine Web: Contient les fichiers de l'interface web. Ils sont normalement placés dans le même répertoire sous ./web"
}
}
-112
View File
@@ -1,112 +0,0 @@
{
"LABEL": {
"LIVE": "In diretta",
"VOD": "VOD",
"SERIES": "Serie",
"SAVE": "Salva",
"SHOW_SELECTED": "Mostra selezionati",
"SELECT_ALL": "Seleziona tutto",
"DESELECT_ALL": "Deseleziona tutto",
"SEARCH": "Cerca",
"CLEAR": "Cancella",
"REGEXP": "Espressione regolare",
"LOGOUT": "Esci",
"CONFIGURATION": "Configurazione",
"HOST": "Host",
"PORT": "Porta",
"WEB_ROOT": "Root web",
"THREADS": "Thread",
"WORKING_DIR": "Cartella di lavoro",
"BACKUP_DIR": "Cartella di backup",
"UPDATE_ON_BOOT": "Aggiorna all'avvio",
"WEB_UI": "Interfaccia web",
"SCHEDULES": "Orari",
"SANITIZE_SENSITIVE_INFO": "Sanitizza informazioni sensibili",
"ACTIVE_CLIENTS": "Client attivi",
"RESOURCE_REWRITE_DISABLE": "Disabilita riscrittura risorse",
"RECONNECT": "Riconnetti",
"BUFFER_ENABLED": "Buffer attivato",
"BUFFER_SIZE": "Dimensione buffer",
"ENABLED": "Abilitato",
"SIZE": "Dimensione",
"CACHE_DIR": "Cartella cache",
"NOTIFY_ON": "Notifica su",
"BOT_TOKEN": "Token bot",
"CHAT_IDS": "ID chat",
"URL": "URL",
"USER": "Utente",
"TOKEN": "Token",
"WEB_SEARCH_URL": "URL ricerca web",
"EXTENSIONS": "Estensioni",
"HEADERS": "Intestazioni",
"DIRECTORY": "Cartella",
"ORGANIZE_INTO_DIRECTORIES": "Organizza in cartelle",
"EPISODE_PATTERN": "Pattern episodio",
"API": "API",
"MAIN": "Principale",
"REVERSE_PROXY": "Proxy inverso",
"MESSAGING": "Messaggistica",
"VIDEO": "Video",
"EXAMPLES": "Esempi",
"UPDATE": "Aggiorna",
"START": "Avvia",
"CACHE": "Cache",
"STREAM": "Flusso",
"LOG": "Registro",
"CONFIG": "Configurazione",
"TELEGRAM": "Telegram",
"REST": "Rest",
"PUSHOVER": "Pushover",
"PROXY": "Proxy",
"REFRESH": "Ricarica",
"STATUS": "Stato",
"MAX_CON": "MaxCon",
"EXP_DATE": "Data di scadenza",
"USERNAME": "Nome utente",
"PASSWORD": "Password",
"SERVER": "Server",
"TIMEZONE": "Fuso orario",
"PATH": "Percorso",
"MESSAGE": "Messaggio",
"PROTOCOL": "Protocollo",
"NAME": "Nome"
},
"TITLE": {
"USER_BOUQUET_EDITOR": "Editor bouquet utente"
},
"MESSAGES": {
"INVALID_REGEXP": "Espressione regolare non valida",
"INVALID_FILETYPE": "Tipo di file non valido",
"DOWNLOAD": {
"SUCCESS": "Scaricato con successo",
"FAIL": "Download fallito!",
"PLAYLIST": {
"SUCCESS": "Playlist scaricata con successo",
"FAIL": "Download playlist fallito!"
},
"SERVER_CONFIG": {
"FAIL": "Download configurazione server fallito!"
}
},
"SAVE": {
"MAIN_CONFIG": {
"SUCCESS": "Configurazione principale salvata!",
"FAIL": "Salvataggio configurazione principale fallito!"
},
"BOUQUET": {
"SUCCESS": "Bouquet salvato con successo",
"FAIL": "Salvataggio bouquet fallito!"
},
"USER": {
"SUCCESS": "Utente salvato con successo",
"FAIL": "Salvataggio utente fallito!"
}
}
},
"INFO": {
"RESTART_TO_APPLY_CHANGES": "È necessario riavviare per applicare le modifiche.",
"SCHEDULE_EXAMPLE": "Esempio di programma",
"THREADS": "Thread: lascia 0 se non hai problemi. Aumenta se hai più provider. L'accesso parallelo ai tuoi provider potrebbe causare un ban.",
"WEB_ROOT": "Root Web: Contiene i file dell'interfaccia web. Di solito vengono posizionati nella stessa cartella sotto ./web"
}
}
-112
View File
@@ -1,112 +0,0 @@
{
"LABEL": {
"LIVE": "Прямой эфир",
"VOD": "VOD",
"SERIES": "Сериалы",
"SAVE": "Сохранить",
"SHOW_SELECTED": "Показать выбранное",
"SELECT_ALL": "Выбрать все",
"DESELECT_ALL": "Отменить выбор всех",
"SEARCH": "Поиск",
"CLEAR": "Очистить",
"REGEXP": "Регулярное выражение",
"LOGOUT": "Выйти",
"CONFIGURATION": "Конфигурация",
"HOST": "Хост",
"PORT": "Порт",
"WEB_ROOT": "Корень веба",
"THREADS": "Потоки",
"WORKING_DIR": "Рабочий каталог",
"BACKUP_DIR": "Каталог резервных копий",
"UPDATE_ON_BOOT": "Обновить при загрузке",
"WEB_UI": "Веб-интерфейс",
"SCHEDULES": "Расписания",
"SANITIZE_SENSITIVE_INFO": "Очистить чувствительную информацию",
"ACTIVE_CLIENTS": "Активные клиенты",
"RESOURCE_REWRITE_DISABLE": "Отключить переписывание ресурсов",
"RECONNECT": "Переподключить",
"BUFFER_ENABLED": "Буфер включен",
"BUFFER_SIZE": "Размер буфера",
"ENABLED": "Включено",
"SIZE": "Размер",
"CACHE_DIR": "Каталог кэша",
"NOTIFY_ON": "Уведомить о",
"BOT_TOKEN": "Токен бота",
"CHAT_IDS": "ID чатов",
"URL": "URL",
"USER": "Пользователь",
"TOKEN": "Токен",
"WEB_SEARCH_URL": "URL веб-поиска",
"EXTENSIONS": "Расширения",
"HEADERS": "Заголовки",
"DIRECTORY": "Каталог",
"ORGANIZE_INTO_DIRECTORIES": "Организовать в каталоги",
"EPISODE_PATTERN": "Шаблон эпизодов",
"API": "API",
"MAIN": "Основное",
"REVERSE_PROXY": "Обратный прокси",
"MESSAGING": "Сообщения",
"VIDEO": "Видео",
"EXAMPLES": "Примеры",
"UPDATE": "Обновить",
"START": "Старт",
"CACHE": "Кэш",
"STREAM": "Поток",
"LOG": "Лог",
"CONFIG": "Конфигурация",
"TELEGRAM": "Телеграм",
"REST": "Rest",
"PUSHOVER": "Pushover",
"PROXY": "Прокси",
"REFRESH": "Обновить",
"STATUS": "Статус",
"MAX_CON": "Макс. соединений",
"EXP_DATE": "Дата истечения",
"USERNAME": "Имя пользователя",
"PASSWORD": "Пароль",
"SERVER": "Сервер",
"TIMEZONE": "Часовой пояс",
"PATH": "Путь",
"MESSAGE": "Сообщение",
"PROTOCOL": "Протокол",
"NAME": "Имя"
},
"TITLE": {
"USER_BOUQUET_EDITOR": "Редактор букета пользователя"
},
"MESSAGES": {
"INVALID_REGEXP": "Неверное регулярное выражение",
"INVALID_FILETYPE": "Неверный тип файла",
"DOWNLOAD": {
"SUCCESS": "Загрузка успешна",
"FAIL": "Ошибка загрузки!",
"PLAYLIST": {
"SUCCESS": "Плейлист успешно загружен",
"FAIL": "Ошибка загрузки плейлиста!"
},
"SERVER_CONFIG": {
"FAIL": "Не удалось загрузить конфигурацию сервера!"
}
},
"SAVE": {
"MAIN_CONFIG": {
"SUCCESS": "Основная конфигурация сохранена!",
"FAIL": "Не удалось сохранить основную конфигурацию!"
},
"BOUQUET": {
"SUCCESS": "Букет успешно сохранен",
"FAIL": "Не удалось сохранить букет!"
},
"USER": {
"SUCCESS": "Пользователь успешно сохранен",
"FAIL": "Не удалось сохранить пользователя!"
}
}
},
"INFO": {
"RESTART_TO_APPLY_CHANGES": "Необходимо перезагрузить систему, чтобы применить изменения.",
"SCHEDULE_EXAMPLE": "Пример расписания",
"THREADS": "Потоки: оставьте 0, если нет проблем. Увеличьте, если у вас несколько провайдеров. Одновременный доступ к нескольким провайдерам может привести к блокировке.",
"WEB_ROOT": "Корень веба: содержит файлы веб-интерфейса. Обычно они размещаются в той же директории ./web"
}
}
-115
View File
@@ -1,115 +0,0 @@
{
"LABEL": {
"LIVE": "Canlı",
"VOD": "Video",
"SERIES": "Diziler",
"SAVE": "Kaydet",
"SUBMIT": "Onayla",
"OK": "Tamam",
"CANCEL": "İptal",
"SHOW_SELECTED": "Seçilenleri Göster",
"SELECT_ALL": "Tümünü Seç",
"DESELECT_ALL": "Tümünü Seçimden Çıkar",
"SEARCH": "Ara",
"CLEAR": "Sil",
"REGEXP": "RegExp",
"LOGOUT": "Çıkış Yap",
"CONFIGURATION": "Yapılandırma",
"HOST": "Sunucu",
"PORT": "Port",
"WEB_ROOT": "Web Klasörü",
"THREADS": "İşlemciler",
"WORKING_DIR": "Çalışma Klasörü",
"BACKUP_DIR": "Yedekleme Klasörü",
"UPDATE_ON_BOOT": "Başlangıçta Güncelle",
"WEB_UI": "Web UI",
"SCHEDULES": "Programlar",
"SANITIZE_SENSITIVE_INFO": "Hassas Bilgileri Temizle",
"ACTIVE_CLIENTS": "Aktif Kullanıcılar",
"RESOURCE_REWRITE_DISABLE": "Url Yönlendirme Devre Dışı",
"RECONNECT": "Yeniden Bağlan",
"BUFFER_ENABLED": "Arabellek Aktif",
"BUFFER_SIZE": "Arabellek Boyutu",
"ENABLED": "Aktif",
"SIZE": "Boyut",
"CACHE_DIR": "Önbellek Dizin",
"NOTIFY_ON": "Bildirimler",
"BOT_TOKEN": "Bot Token'i",
"CHAT_IDS": "Chat ID'leri",
"URL": "URL",
"USER": "Kullanıcı",
"TOKEN": "Token",
"WEB_SEARCH_URL": "Web Arama URL'si",
"EXTENSIONS": "Eklentiler",
"HEADERS": "Başlıklar",
"DIRECTORY": "Klasör",
"ORGANIZE_INTO_DIRECTORIES": "Klasörlere ayır",
"EPISODE_PATTERN": "Bölüm Pattern'i",
"API": "API",
"MAIN": "Ana",
"REVERSE_PROXY": "Ters Proxy",
"MESSAGING": "Mesajlaşma",
"VIDEO": "Video",
"EXAMPLES": "Örnekler",
"UPDATE": "Güncelle",
"START": "Başlat",
"CACHE": "Önbellek",
"STREAM": "Yayın",
"LOG": "Kayıt",
"CONFIG": "Yapılandırma",
"TELEGRAM": "Telegram",
"REST": "Rest",
"PUSHOVER": "Pushover",
"PROXY": "Proxy",
"REFRESH": "Yenile",
"STATUS": "Durum",
"MAX_CON": "Max. Bağlantı",
"EXP_DATE": "Son Kullanma Tarihi",
"USERNAME": "Kullanıcı Adı",
"PASSWORD": "Şifre",
"SERVER": "Sunucu",
"TIMEZONE": "Zaman Dilimi",
"PATH": "Yol",
"MESSAGE": "Mesaj",
"PROTOCOL": "Protokol",
"NAME": "Ad"
},
"TITLE": {
"USER_BOUQUET_EDITOR": "Kullanıcı Buket Editörü"
},
"MESSAGES": {
"INVALID_REGEXP": "Geçersiz regexp",
"INVALID_FILETYPE": "Geçersiz dosya türü",
"DOWNLOAD": {
"SUCCESS": "Başarıyla indirildi",
"FAIL": "İndirme başarısız!",
"PLAYLIST": {
"SUCCESS": "Liste başarıyla indirildi",
"FAIL": "Liste indirilemedi!"
},
"SERVER_CONFIG": {
"FAIL": "Sunucu yapılandırması indirilemedi!"
}
},
"SAVE": {
"MAIN_CONFIG": {
"SUCCESS": "Ana yapılandırma kaydedildi!",
"FAIL": "Ana yapılandırma kaydedilemedi!"
},
"BOUQUET": {
"SUCCESS": "Buket başarıyla kaydedildi",
"FAIL": "Buket kaydedilemedi!"
},
"USER": {
"SUCCESS": "Kullanıcı başarıyla kaydedildi",
"FAIL": "Kullanıcı kaydedilemedi!"
}
}
},
"INFO": {
"RESTART_TO_APPLY_CHANGES": "Değişiklikleri uygulamak için yeniden başlatmanız gerekmektedir.",
"SCHEDULE_EXAMPLE": "Program örneği",
"THREADS": "İşlemciler: Eğer bir sorununuz yoksa 0 olarak bırakın. Birden fazla sağlayıcı kullanıyorsanız artırabilirsiniz. Sağlayıcınıza paralel erişim, yasaklanmanıza neden olabilir.",
"WEB_ROOT": "Web klassörü: Web-UI dosyalarını içerir. Genellikle ./web altında aynı dizine yerleştirilir."
}
}
+7 -4
View File
@@ -1,9 +1,12 @@
{
"tab_title": "tuliprox",
"app_title": "tuliprox",
"app_logo": "assets/tuliprox-logo.svg",
"tabTitle": "tuliprox",
"appLogo": "/assets/tuliprox-logo.svg",
"api": {
"apiUrl": "/api/v1/",
"authUrl": "/auth/"
}
},
"discord": "https://discord.gg/gkzCmWw9Tf",
"documentation": "https://github.com/euzu/tuliprox/blob/master/README.md",
"wsUrl" : "/ws",
"protocolVersion": 1
}
-28
View File
@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/">
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/assets/favicon.ico" />
<link rel="css" href="%PUBLIC_URL%/assets/fonts.css" />
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!-- link rel="manifest" href="%PUBLIC_URL%/assets/manifest.json" / -->
<title>tuliprox</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
-15
View File
@@ -1,15 +0,0 @@
{
"short_name": "tuliprox",
"name": "tuliprox",
"icons": [
{
"src": "assets/favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
-3
View File
@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
@@ -271,6 +271,30 @@
--menu-icon-color-main_config: #FF2DF1;
--menu-icon-color-status: #F9E400;
--code-identifier-color: #569cd6;
--code-var-access-color: #9cdcfe;
--code-field-color: #bb8953;
--code-property-color: #9a652f;
--code-literal-color: #65a181;
--code-num-literal-color: #6796ba;
--code-built-in-function-color: #fac46c;
--code-reserved-color: #e98863;
--code-regex-color: #bb8953;
--code-enum-color: #65a181;
--code-range-color: #6796ba;
--code-op-color: #569cd6;
--code-comparison-background-color: #202429;
--code-comparison-color: #ffffff;
--code-bracket-1-color: #569CD6;
--code-bracket-2-color: #C586C0;
--code-bracket-3-color: #B5CEA8;
--code-editor-background-color: #1e1e1e;
--code-editor-color: #d4d4d4;
--code-any-match-color: #fa5f2d;
--code-null-literal-color: #fa5f2d;
--toolbar-bg-color: #161b25;
}
@@ -48,6 +48,8 @@
@forward "components/dialog";
@forward "components/custom_dialog";
@forward "components/filter";
@forward "components/mapper_script";
@forward "components/mapper_counter";
@forward "components/userlist/user_state";
@forward "components/userlist/proxy_type";
@forward "components/userlist/max_connections";
@@ -57,3 +59,4 @@
@forward "components/loading_indicator";
@forward "components/toastr";
@forward "components/drop_down_icon_button";
@forward "components/accordion";
@@ -0,0 +1,7 @@
.tp__accordion {
display: flex;
flex-flow: column;
overflow: hidden;
box-sizing: border-box;
width: 100%;
}
@@ -2,7 +2,7 @@
display: flex;
flex-flow: column;
overflow: hidden;
flex-shrink: 0;
//flex-shrink: 0;
&__header {
display: flex;
@@ -35,10 +35,10 @@
&__body {
display: flex;
flex-flow: column;
flex-shrink: 0;
width: 100%;
box-sizing: border-box;
height: fit-content;
overflow: hidden;
// transition: max-height 0.5s ease;
// max-height: 5000px;
}
@@ -73,6 +73,7 @@
display: flex;
justify-content: flex-end;
border-top: 1px solid var(--border-color);
padding-top: var(--padding-small);
gap: var(--gap-default);
&-left {
@@ -5,8 +5,8 @@
.tp__filter__code {
font-family: monospace;
background: #1e1e1e;
color: #d4d4d4;
background: var(--code-editor-background-color);
color: var(--code-editor-color);
padding: 1rem;
border-radius: 8px;
white-space: pre;
@@ -14,38 +14,40 @@
.binary_op-wrapper{
white-space: pre;
}
.comparison {
white-space: nowrap;
overflow: hidden;
background-color: #202429;
color: var(--code-comparison-color);
background-color: var(--code-comparison-background-color);
padding: var(--padding-micro);
border-radius: var(--border-radius);
}
.unary_op {
font-weight: bold;
color: #569cd6;
color: var(--code-op-color);
text-transform: uppercase;
}
.binary_op {
font-weight: bold;
color: #9cdcfe;
color: var(--code-op-color);
text-transform: uppercase;
}
.field {
font-weight: bold;
color: #fdb76c;
color: var(--code-field-color);
}
.regex {
color: #bb8953;
color: var(--code-regex-color);
white-space: nowrap;
}
.enum {
color: #78ce89;
color: var(--code-enum-color);
}
}
@@ -54,12 +56,9 @@
font-size: 1.1rem;
}
//.bracket-1 { color: #c551ac; }
//.bracket-2 { color: #70db39; }
//.bracket-3 { color: #dbdb02; }
.bracket-1 { color: #569CD6; }
.bracket-2 { color: #C586C0; }
.bracket-3 { color: #B5CEA8; }
.bracket-1 { color: var(--code-bracket-1-color); }
.bracket-2 { color: var(--code-bracket-2-color); }
.bracket-3 { color: var(--code-bracket-3-color); }
}
.tp__filter__inline {
.tp__filter__code {
@@ -0,0 +1,20 @@
.tp__mapper-counter {
display: flex;
flex-flow: column;
overflow: auto;
box-sizing: border-box;
&__row {
display: flex;
flex-flow: row;
box-sizing: border-box;
label {
color: var(--modest-text-color);
}
}
}
.tp__mapper-counter__inline {
flex-flow: row;
overflow: hidden;
}
@@ -0,0 +1,77 @@
.tp__mapper-script {
display: flex;
flex-flow: column;
overflow: auto;
.tp__mapper-script__code {
font-family: monospace;
background: #1e1e1e;
color: var(--modest-text-color);
padding: var(--padding-default);
border-radius: var(--border-radius);
white-space: pre;
.identifier {
color: var(--code-identifier-color);
}
.var-access {
color: var(--code-var-access-color);
}
.field {
color: var(--code-field-color);
}
.property {
color: var(--code-property-color);
}
.literal {
color: var(--code-literal-color);
}
.num-literal {
color: var(--code-num-literal-color);
}
.built-in-function {
color: var(--code-built-in-function-color);
}
.reserved {
color: var(--code-reserved-color);
}
.any-match {
color: var(--code-any-match-color);
font-weight: bold;
}
.range {
color: var(--code-range-color);
font-weight: bold;
}
.null-literal {
color: var(--code-null-literal-color);
font-weight: bold;
}
}
.bracket {
font-weight: bold;
font-size: 1.1rem;
}
.bracket-1 { color: #569CD6; }
.bracket-2 { color: #C586C0; }
.bracket-3 { color: #B5CEA8; }
}
.tp__mapper-script__inline {
.tp__mapper-script__code {
padding: 0;
margin-block: 0;
}
}
@@ -0,0 +1,64 @@
@use '../../../../scss/size';
.tp__playlist-mappings {
display: flex;
flex-flow: column;
justify-content: center;
align-items: flex-start;
box-sizing: border-box;
overflow: hidden;
&__list {
display: flex;
flex-flow: column;
gap: var(--gap-default);
box-sizing: border-box;
overflow: hidden;
}
&__mapping {
display: flex;
flex-flow: column;
gap: var(--gap-default);
justify-content: center;
box-sizing: border-box;
overflow: hidden;
&-section {
display: flex;
flex-flow: row ;
align-items: center;
box-sizing: border-box;
gap: var(--gap-small);
overflow: hidden;
}
&-mapper {
display: flex;
flex-flow: column;
gap: var(--gap-default);
box-sizing: border-box;
overflow: auto;
&-content {
display: flex;
flex-flow: column;
gap: var(--gap-default);
justify-content: center;
align-items: flex-start;
box-sizing: border-box;
overflow: hidden;
}
}
}
label {
font-weight: bold;
color: var(--modest-text-color);
&:after {
content: ':'
}
}
}
@@ -1,7 +1,7 @@
.tp__playlist-update-view {
display: flex;
flex-flow: column;
width: 100%;
width: 80vw;
gap: var(--gap-default);
padding: var(--padding-default) 0;
box-sizing: border-box;
@@ -17,6 +17,7 @@
&__body {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--gap-default);
box-sizing: border-box;
overflow-y: auto;
@@ -1,6 +1,12 @@
@use "../../../size";
.tp__playlist-source-selector {
display: flex;
flex-flow: column;
gap: var(--gap-default);
box-sizing: border-box;
width: 100%;
flex-shrink: 0;
&__source-picker .tp__collapse-panel__body {
display: flex;

Some files were not shown because too many files have changed in this diff Show More