Split main crate into workspace subcrates (cli/core/icons/translations/web)

This commit is contained in:
Simon Repp
2025-09-05 10:17:11 +02:00
parent 2ee0f0cac6
commit 20a1486885
246 changed files with 3321 additions and 2812 deletions
-1
View File
@@ -1,5 +1,4 @@
# SPDX-FileCopyrightText: 2024-2025 Simon Repp
# SPDX-License-Identifier: CC0-1.0
/docs/target/
/target/
+1 -1
View File
@@ -36,7 +36,7 @@ cd hyper8
Now run this command:
```bash
cargo install --locked --path .
cargo install --locked --path cli
```
## Uninstalling
Generated
+799 -629
View File
File diff suppressed because it is too large Load Diff
+22 -12
View File
@@ -1,19 +1,23 @@
# SPDX-FileCopyrightText: 2024-2025 Simon Repp
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
edition = "2021"
name = "hyper8"
rust-version = "1.82"
version = "0.23.0"
[workspace]
members = [
"cli",
"core",
"docs",
"icons",
"translations",
"version",
"web"
]
resolver = "2"
[build-dependencies]
base64 = "0.22.1"
[dependencies]
[workspace.dependencies]
actix-files = "0.6.6"
actix-multipart = "0.7.2"
actix-web = "4.11.0"
actix-session = { features = ["cookie-session"], version = "0.11.0" }
actix-web = { version = "4.11.0" }
base64 = "0.22.1"
# bincode 2.x is available but it includes breaking changes (on the API level,
# encoded data is "completely compatible if the same configuration is used")
@@ -21,7 +25,7 @@ base64 = "0.22.1"
# update.
bincode = "1.3.3"
chrono = { features = ["unstable-locales"], version = "0.4.41" }
clap = { features = ["derive"], version = "4.5.40" }
clap = "4.5.40"
dirs = "6.0.0"
dunce = "1.0.5"
enolib = { git = "https://codeberg.org/simonrepp/enolib-rs", tag = "0.5.0" }
@@ -37,6 +41,7 @@ log = "0.4.27"
minreq = { features = ["https-rustls"], version = "2.13.4" }
nanoid = "0.4.0"
notify = "8.0.0"
pulldown-cmark = { default-features = false, features = ["html", "simd"], version = "0.13.0" }
rand = "0.9.1"
rsubs-lib = "0.3.3"
serde = "1.0.219"
@@ -46,7 +51,12 @@ slug = "0.1.6"
suppaftp = { features = ["native-tls"], version = "6.3.0" }
sys-locale = "0.3.2"
tokio = { features = ["macros", "rt-multi-thread"], version = "1.45.1" }
uuid = { features = ["v5"], version = "1.17.0" }
url = "2.5.4"
urlencoding = "2.1.3"
uuid = { features = ["v5"], version = "1.17.0" }
webbrowser = "1.0.5"
[workspace.package]
edition = "2021"
rust-version = "1.82"
version = "0.23.0"
-70
View File
@@ -1,70 +0,0 @@
// SPDX-FileCopyrightText: 2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
//! Build hyper8 with (e.g.) HYPER8_VERSION=2.0.0~pre1 to override
//! the version that is displayed and reported in resulting builds.
use std::env;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::process::Command;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
/// Provides four variables to the consecutive build step:
/// - HYPER8_REVISION (If git is available the short commit hash, otherwise "unknown revision")
/// - HYPER8_VERSION_ADAPTIVE (The version down to patch level only if it is a patch release, e.g. "1.2.1")
/// - HYPER8_VERSION_DETAILED (The version down to patch level, e.g. "1.2.0")
/// - HYPER8_VERSION_DISPLAY (The version for pretty display, without patch level, e.g. "1.2")
///
/// If HYPER8_VERSION is used to override the version, both HYPER8_VERSION_DETAILED and
/// HYPER8_VERSION_DISPLAY will contain its value, e.g. "2.0.0~pre1".
fn main() {
let version_adaptive;
let version_detailed;
let version_display;
if let Ok(override_version) = env::var("HYPER8_VERSION") {
version_adaptive = override_version.clone();
version_detailed = override_version.clone();
version_display = override_version;
} else {
version_adaptive = match env!("CARGO_PKG_VERSION_PATCH") {
"0" => concat!(env!("CARGO_PKG_VERSION_MAJOR"), '.', env!("CARGO_PKG_VERSION_MINOR")).to_string(),
_ => env!("CARGO_PKG_VERSION").to_string()
};
version_detailed = env!("CARGO_PKG_VERSION").to_string();
version_display = concat!(env!("CARGO_PKG_VERSION_MAJOR"), '.', env!("CARGO_PKG_VERSION_MINOR")).to_string();
}
let mut git = Command::new("git");
git.args(["rev-parse", "--short", "HEAD"]);
let revision = match git.output() {
Ok(output) => String::from_utf8(output.stdout).unwrap(),
Err(_) => String::from("unknown revision")
};
println!("cargo:rerun-if-env-changed=HYPER8_VERSION");
println!("cargo:rustc-env=HYPER8_REVISION={revision}");
println!("cargo:rustc-env=HYPER8_VERSION_ADAPTIVE={version_adaptive}");
println!("cargo:rustc-env=HYPER8_VERSION_DETAILED={version_detailed}");
println!("cargo:rustc-env=HYPER8_VERSION_DISPLAY={version_display}");
let static_asset_hash_adaptive_theme_js = url_safe_hash_base64(include_bytes!("src/build/assets/adaptive_theme.js"));
let static_asset_hash_clipboard_js = url_safe_hash_base64(include_bytes!("src/build/assets/clipboard.js"));
let static_asset_hash_embed_js = url_safe_hash_base64(include_bytes!("src/build/assets/embed.js"));
let static_asset_hash_favicon_png = url_safe_hash_base64(include_bytes!("src/build/assets/favicon.png"));
let static_asset_hash_video_js = url_safe_hash_base64(include_bytes!("src/build/assets/video.js"));
println!("cargo:rustc-env=HYPER8_STATIC_ASSET_HASH_ADAPTIVE_THEME_JS={static_asset_hash_adaptive_theme_js}");
println!("cargo:rustc-env=HYPER8_STATIC_ASSET_HASH_CLIPBOARD_JS={static_asset_hash_clipboard_js}");
println!("cargo:rustc-env=HYPER8_STATIC_ASSET_HASH_EMBED_JS={static_asset_hash_embed_js}");
println!("cargo:rustc-env=HYPER8_STATIC_ASSET_HASH_FAVICON_PNG={static_asset_hash_favicon_png}");
println!("cargo:rustc-env=HYPER8_STATIC_ASSET_HASH_VIDEO_JS={static_asset_hash_video_js}");
}
pub fn url_safe_hash_base64(hashable: &impl Hash) -> String {
let mut hasher = DefaultHasher::new();
hashable.hash(&mut hasher);
let hash = hasher.finish();
URL_SAFE_NO_PAD.encode(hash.to_le_bytes())
}
+23
View File
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2024-2025 Simon Repp
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
edition.workspace = true
name = "hyper8-cli"
rust-version.workspace = true
version.workspace = true
[dependencies]
base64.workspace = true
clap.workspace = true
dunce.workspace = true
enolib.workspace = true
env_logger.workspace = true
hyper8-core.path = "../core"
hyper8-translations.path = "../translations"
hyper8-version.path = "../version"
hyper8-web.path = "../web"
indoc.workspace = true
slug.workspace = true
sys-locale.workspace = true
webbrowser.workspace = true
+86
View File
@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::net::IpAddr;
use std::path::PathBuf;
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap::value_parser;
use hyper8_version::VERSION_WITH_PATCH_AND_REVISION;
pub fn parse() -> ArgMatches {
let build = Arg::new("build")
.action(ArgAction::SetTrue)
.help("Directly builds the site without starting the editor")
.long("build")
.short('b');
let build_dir = Arg::new("build-dir")
.help("The path to which the deployable, built site is written")
.long("build-dir")
.value_parser(value_parser!(PathBuf));
let cache_dir = Arg::new("cache-dir")
.help("The path at which all computational results are stored (computed metadata, images and videos)")
.long("cache-dir")
.value_parser(value_parser!(PathBuf));
let deploy = Arg::new("deploy")
.action(ArgAction::SetTrue)
.help("Directly deploys the site - only applicable in combination with --build")
.long("deploy")
.short('d');
let ip = Arg::new("ip")
.help("Manually sets the ip address used by the web editor (otherwise defaults to localhost)")
.long("ip")
.value_parser(value_parser!(IpAddr));
let port = Arg::new("port")
.help("Manually sets the port used by the web editor (otherwise hyper8 chooses an available port on its own)")
.long("port")
.value_parser(value_parser!(u16));
let preview = Arg::new("preview")
.action(ArgAction::SetTrue)
.help("Use in conjunction with --build to immediately open the site in the browser after the build is complete")
.long("preview")
.short('p');
let site_dir = Arg::new("site-dir")
.help("The path at which the site resides (user videos and metadata)")
.required(true)
.value_parser(value_parser!(PathBuf));
let workers = Arg::new("workers")
.default_value("2")
.help("The number of threads to run for background processing (metadata computation, image processing, video/audio transcoding, etc.)")
.long("workers")
.value_parser(worker_count);
let command = Command::new("Hyper 8 Video System")
.version(VERSION_WITH_PATCH_AND_REVISION)
.about("A static site generator for video publishing")
.arg(build)
.arg(build_dir)
.arg(cache_dir)
.arg(deploy)
.arg(ip)
.arg(port)
.arg(preview)
.arg(site_dir)
.arg(workers);
command.get_matches()
}
fn worker_count(string: &str) -> Result<usize, String> {
match string.parse::<usize>() {
Ok(number) => match number >= 2 {
true => Ok(number),
false => Err(String::from("At least 2 workers required"))
}
Err(err) => Err(err.to_string())
}
}
+75
View File
@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::io;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use hyper8_core::{Context, Site};
use hyper8_core::{config_probe, start_deploy};
use hyper8_translations::Translations;
/// Performs deployment in a synchronous fashion, reporting back all messages
/// and errors to the terminal. Also asks for explicit ("opt-in") confirmation
/// before performing the actual deployment.
pub fn perform_deploy_blocking_cli(
context_ref: &Arc<Context>,
site: &Site,
translations: &Translations
) -> Result<(), String> {
if site.base_url.is_none() {
let site_manifest_path = context_ref.site_dir.join("site.eno");
let message = translations.public_address_must_be_configured_for_deployment_text_cli(&site_manifest_path);
return Err(message);
}
if !site.complete_deployment_config() {
let message = translations.deployment_configuration_is_incomplete_can_not_deploy.to_string();
return Err(message);
}
let t_really_deploy = translations.really_deploy;
let base_url = &site.base_url.as_ref().unwrap().representation;
println!("{t_really_deploy} ({base_url})");
println!("{}", translations.type_y_and_press_enter_to_confirm_anything_else_will_abort);
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(_) => {
if input.trim() != "y" {
return Ok(());
}
}
Err(err) => return Err(err.to_string())
}
if let Err(err) = config_probe(context_ref, site, translations) {
let t_deployment_configuration_could_not_be_verified = translations.deployment_configuration_could_not_be_verified;
let message = format!("{t_deployment_configuration_could_not_be_verified} ({err})");
return Err(message);
}
start_deploy(context_ref, site, translations)?;
let mut seen_feedback = 0;
loop {
thread::sleep(Duration::from_secs(1));
if let Ok(deployment) = context_ref.deploying.lock() {
let feedback = &deployment.feedback;
if feedback.len() > seen_feedback {
eprintln!("{}", &feedback[seen_feedback..]);
seen_feedback = feedback.len();
}
if !deployment.active() {
break;
}
}
}
Ok(())
}
+145
View File
@@ -0,0 +1,145 @@
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::net::IpAddr;
use std::path::PathBuf;
use std::process::ExitCode;
use std::sync::{Arc, Mutex};
use sys_locale::get_locale;
use hyper8_core::{
Cache,
Context,
Language,
Site
};
use hyper8_core::{perform_build, start_preview_server, start_workers};
use hyper8_core::watcher::watch_site_dir;
mod args;
mod deploy;
use deploy::perform_deploy_blocking_cli;
fn main() -> ExitCode {
env_logger::init();
let language = get_locale()
.map(|locale| Language::from_code(&locale))
.unwrap_or_else(|| Language::default());
let translations = &language.translations;
let args = args::parse();
let site_dir = args.get_one::<PathBuf>("site-dir").unwrap();
if !site_dir.exists() {
eprintln!(
"Site directory {site_dir:?} does not exist, please create it manually (just an empty folder).",
);
return ExitCode::FAILURE;
}
// This compiles to fs::canonicalize(...) on all platforms but windows,
// on windows it instead tries to supply a canonicalized path that uses
// the regular form (e.g. "C:\\Foo") instead of the UNC form ("\\?C:\\Foo")
// when possible (for background see https://crates.io/crates/dunce).
let site_dir = dunce::canonicalize(site_dir).unwrap();
let build_dir = args.get_one::<PathBuf>("build-dir")
.as_ref()
.map(|path| path.to_path_buf())
.unwrap_or_else(|| site_dir.join(".hyper8_build"));
let cache_dir = args.get_one::<PathBuf>("cache-dir")
.as_ref()
.map(|path| path.to_path_buf())
.unwrap_or_else(|| site_dir.join(".hyper8_cache"));
let cache = Mutex::new(Cache::retrieve(&cache_dir));
let context = Context::new(
build_dir,
cache,
cache_dir,
Arc::new(Site::new()),
site_dir.clone()
);
// TODO: Would be great if we could somehow resolve the conundrum that
// context depends on site and site depends on context, which
// forces us to initialize context with a dummy site and replace it here.
context.replace_site(Site::read_dir(&context, &site_dir));
let context = Arc::new(context);
let workers = args.get_one::<usize>("workers").unwrap();
context.compute_job_queue();
start_workers(&context, *workers, &translations);
let ip = args.get_one::<IpAddr>("ip");
let port = args.get_one::<u16>("port");
if args.get_flag("build") {
if let Err(err) = context.await_job_queue() {
eprintln!("{err}");
return ExitCode::FAILURE;
}
perform_build(&context);
if args.get_flag("preview") {
let site = context.get_site();
if site.clean_urls {
// Here we serve the preview through an actual http server so
// that /foo/ gets resolved to /foo/index.html.
let result = start_preview_server(&context.build_dir, ip, port);
if result.is_ok() { ExitCode::SUCCESS } else { ExitCode::FAILURE }
} else {
// We don't need an actively running server to preview a build
// without clean urls, we can just open everything directly in
// a browser.
let local_file_url = context.build_dir.join("index.html");
if webbrowser::open(&local_file_url.to_string_lossy()).is_err() {
eprintln!("Could not open browser for previewing the site");
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
} else if args.get_flag("deploy") {
let site = context.get_site();
match perform_deploy_blocking_cli(&context, &site, &translations) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
}
} else {
ExitCode::SUCCESS
}
} else {
println!("Using site directory: {}", context.site_dir.display());
watch_site_dir(Arc::clone(&context));
let result = hyper8_web::start_server(
context,
ip,
port
);
if result.is_ok() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
}
+44
View File
@@ -0,0 +1,44 @@
# SPDX-FileCopyrightText: 2024-2025 Simon Repp
# SPDX-License-Identifier: AGPL-3.0-or-later
[package]
edition.workspace = true
name = "hyper8-core"
rust-version.workspace = true
version.workspace = true
[build-dependencies]
base64.workspace = true
[dependencies]
actix-files.workspace = true
actix-web.workspace = true
base64.workspace = true
bincode.workspace = true
chrono.workspace = true
enolib.workspace = true
form_urlencoded.workspace = true
hyper8-icons.path = "../icons"
hyper8-translations.path = "../translations"
hyper8-version.path = "../version"
icu_collator.workspace = true
icu_locale.workspace = true
image.workspace = true
indoc.workspace = true
language-tags.workspace = true
log.workspace = true
minreq.workspace = true
nanoid.workspace = true
notify.workspace = true
rand.workspace = true
rsubs-lib.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
slug.workspace = true
suppaftp.workspace = true
tokio.workspace = true
uuid.workspace = true
url.workspace = true
urlencoding.workspace = true
webbrowser.workspace = true
+46
View File
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::hash::{DefaultHasher, Hash, Hasher};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
fn main() {
provide_asset_hash(
include_bytes!("src/build/assets/adaptive_theme.js"),
"ADAPTIVE_THEME_JS"
);
provide_asset_hash(
include_bytes!("src/build/assets/clipboard.js"),
"CLIPBOARD_JS"
);
provide_asset_hash(
include_bytes!("src/build/assets/embed.js"),
"EMBED_JS"
);
provide_asset_hash(
include_bytes!("src/build/assets/favicon.png"),
"FAVICON_PNG"
);
provide_asset_hash(
include_bytes!("src/build/assets/video.js"),
"VIDEO_JS"
);
}
fn provide_asset_hash(bytes: &[u8], key: &str) {
let hash = url_safe_hash_base64(bytes);
println!("cargo:rustc-env=HYPER8_STATIC_ASSET_HASH_{key}={hash}");
}
pub fn url_safe_hash_base64(content: &[u8]) -> String {
let mut hasher = DefaultHasher::new();
content.hash(&mut hasher);
let hash = hasher.finish();
URL_SAFE_NO_PAD.encode(hash.to_le_bytes())
}
@@ -95,4 +95,4 @@ pub fn video_analysis(
} else {
None
}
}
}
+6 -30
View File
@@ -6,6 +6,7 @@ use std::path::Path;
use std::thread;
use std::time::Duration;
use hyper8_version::{VERSION_WITH_PATCH_AND_REVISION};
use rsubs_lib::SRT;
use crate::{
@@ -20,6 +21,7 @@ use crate::{
use crate::build::widgets::thumbnail;
use crate::util::{ensure_empty_dir, hard_link_or_copy};
mod asset_hashes;
mod collection;
mod download;
mod embed;
@@ -45,38 +47,12 @@ use playlist::playlist_html;
use subscribe::subscribe_html;
use video::video_html;
pub const GENERATOR_INFO: &str = concat!("Hyper 8 Video System ", env!("HYPER8_VERSION_DETAILED"), " (", env!("HYPER8_REVISION"), ")");
/// Placed in the head of each page to identify the Hyper 8 build that generated it.
const META_GENERATOR: &str = concat!(r#"<meta name="generator" content="Hyper 8 Video System "#, env!("HYPER8_VERSION_DETAILED"), " (", env!("HYPER8_REVISION"), r#")">"#);
use asset_hashes::AssetHashes;
const META_ROBOTS_NOINDEX_NOFOLLOW: &str = r#"<meta name="robots" content="noindex, nofollow">"#;
/// When we link to assets on the rendered pages, we append a unique asset
/// hash to each path (e.g. "site.js?g1VVfPoEjUw"), which is derived from
/// the file content of the asset. We do this in order to prompt browsers to
/// fetch new, uncached assets when their content has changed. This struct
/// groups together those hashes for all assets we use.
pub struct AssetHashes {
pub embed_css: Option<String>,
pub navigation_js: Option<String>,
pub site_css: Option<String>
}
impl AssetHashes {
pub const ADAPTIVE_THEME_JS: &str = env!("HYPER8_STATIC_ASSET_HASH_ADAPTIVE_THEME_JS");
pub const CLIPBOARD_JS: &str = env!("HYPER8_STATIC_ASSET_HASH_CLIPBOARD_JS");
pub const EMBED_JS: &str = env!("HYPER8_STATIC_ASSET_HASH_EMBED_JS");
pub const FAVICON_PNG: &str = env!("HYPER8_STATIC_ASSET_HASH_FAVICON_PNG");
pub const VIDEO_JS: &str = env!("HYPER8_STATIC_ASSET_HASH_VIDEO_JS");
pub fn new() -> AssetHashes {
AssetHashes {
embed_css: None,
navigation_js: None,
site_css: None
}
}
pub fn meta_generator() -> String {
format!(r#"<meta name="generator" content="Hyper 8 Video System {VERSION_WITH_PATCH_AND_REVISION}">"#)
}
pub fn perform_build(context: &Context) {
@@ -87,7 +63,7 @@ pub fn perform_build(context: &Context) {
thread::sleep(Duration::from_secs(1));
site = context.get_site();
}
ensure_empty_dir(&context.build_dir);
let mut asset_hashes = AssetHashes::new();
+29
View File
@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
/// When we link to assets on the rendered pages, we append a unique asset
/// hash to each path (e.g. "site.js?g1VVfPoEjUw"), which is derived from
/// the file content of the asset. We do this in order to prompt browsers to
/// fetch new, uncached assets when their content has changed. This struct
/// groups together those hashes for all assets we use.
pub struct AssetHashes {
pub embed_css: Option<String>,
pub navigation_js: Option<String>,
pub site_css: Option<String>
}
impl AssetHashes {
pub const ADAPTIVE_THEME_JS: &str = env!("HYPER8_STATIC_ASSET_HASH_ADAPTIVE_THEME_JS");
pub const CLIPBOARD_JS: &str = env!("HYPER8_STATIC_ASSET_HASH_CLIPBOARD_JS");
pub const EMBED_JS: &str = env!("HYPER8_STATIC_ASSET_HASH_EMBED_JS");
pub const FAVICON_PNG: &str = env!("HYPER8_STATIC_ASSET_HASH_FAVICON_PNG");
pub const VIDEO_JS: &str = env!("HYPER8_STATIC_ASSET_HASH_VIDEO_JS");
pub fn new() -> AssetHashes {
AssetHashes {
embed_css: None,
navigation_js: None,
site_css: None
}
}
}

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -6,11 +6,13 @@ use std::fs;
use indoc::formatdoc;
use crate::{Context, Playlist, Site, Video};
use crate::build::{META_GENERATOR, META_ROBOTS_NOINDEX_NOFOLLOW};
use crate::build::{META_ROBOTS_NOINDEX_NOFOLLOW};
use crate::build::AssetHashes;
use crate::build::outline::Traversal;
use crate::build::player::{player, player_icon_templates};
use super::meta_generator;
/// Writes html for the embed itself, i.e. what is pulled in from external
/// site B through an iframe when they embed a video from site A.
pub fn embed_html(
@@ -51,7 +53,7 @@ pub fn embed_layout(
site: &Site,
traversal: &Traversal
) -> String {
let meta_generator = META_GENERATOR;
let meta_generator = meta_generator();
let meta_robots_noindex_nofollow = META_ROBOTS_NOINDEX_NOFOLLOW;
let embed_css_asset_hash = asset_hashes.embed_css.as_ref().unwrap();
@@ -4,13 +4,13 @@
use std::sync::Arc;
use chrono::Utc;
use hyper8_translations::Translations;
use indoc::formatdoc;
use crate::{
Context,
Site,
SitePath,
Translations,
Video
};
@@ -11,6 +11,8 @@ use std::sync::Arc;
use chrono::{DateTime, Utc};
use indoc::formatdoc;
use hyper8_version::{VERSION_WITH_PATCH, VERSION_WITH_PATCH_AND_REVISION};
use crate::{
Context,
MarkdownSubset,
@@ -18,7 +20,6 @@ use crate::{
SitePath,
Video
};
use crate::build::GENERATOR_INFO;
use crate::util::html_escape_outside_attribute;
use super::ATOM_FILENAME;
@@ -82,15 +83,14 @@ pub fn atom(
let title_escaped = html_escape_outside_attribute(title);
let version_detailed = env!("HYPER8_VERSION_DETAILED");
let xml = formatdoc!(r#"
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<author>
<name>{author_name}</name>
</author>
<generator uri="https://simonrepp.com/hyper8" version="{version_detailed}">
{GENERATOR_INFO}
<generator uri="https://simonrepp.com/hyper8" version="{VERSION_WITH_PATCH}">
Hyper 8 Video System {VERSION_WITH_PATCH_AND_REVISION}
</generator>
<id>{site_url}</id>
<link href="{atom_feed_url}" rel="self"/>
@@ -15,8 +15,9 @@ use std::sync::Arc;
use chrono::{DateTime, Utc};
use indoc::formatdoc;
use hyper8_version::{VERSION_WITH_PATCH_AND_REVISION, VERSION_WITHOUT_PATCH};
use crate::{MarkdownSubset, Site, Video};
use crate::build::GENERATOR_INFO;
use crate::util::html_escape_outside_attribute;
pub fn rss(
@@ -54,7 +55,7 @@ pub fn rss(
} else {
// TODO: Eventually find something better to fallback to.
// Note that this is a mandatory field in RSS (https://www.rssboard.org/rss-specification#requiredChannelElements)
format!("Hyper 8 Video System {}", env!("HYPER8_VERSION_DISPLAY"))
format!("Hyper 8 Video System {VERSION_WITHOUT_PATCH}")
};
let title_escaped = html_escape_outside_attribute(title);
@@ -99,7 +100,7 @@ pub fn rss(
<channel>
<atom:link href="{feed_url}" rel="self" type="application/rss+xml"/>
<description>{description}</description>
<generator>{GENERATOR_INFO}</generator>
<generator>Hyper 8 Video System {VERSION_WITH_PATCH_AND_REVISION}</generator>
<language>{language}</language>
<lastBuildDate>{build_time_rfc2822}</lastBuildDate>
<link>{link}</link>
@@ -3,15 +3,17 @@
use indoc::formatdoc;
use hyper8_version::VERSION_WITHOUT_PATCH;
use crate::{PlatformIntegrationMeta, Site, ThemeKind};
use crate::build::{AssetHashes, Traversal};
use crate::build::feeds::meta_link_tags;
use crate::build::player::player_icon_templates;
use crate::build::widgets::copy_button_icon_templates;
use crate::icons;
use crate::util::html_escape_outside_attribute;
use super::{META_GENERATOR, META_ROBOTS_NOINDEX_NOFOLLOW};
use super::{META_ROBOTS_NOINDEX_NOFOLLOW};
use super::meta_generator;
pub struct Layout {
clipboard_script: bool,
@@ -106,9 +108,9 @@ impl Layout {
let navigation;
let navigation_script;
if let Some(navigation_js_asset_hash) = &asset_hashes.navigation_js {
let browse_icon = icons::BROWSE;
let close_icon = icons::failure(translations.close);
let search_icon = icons::search(translations.search);
let browse_icon = hyper8_icons::BROWSE;
let close_icon = hyper8_icons::failure(translations.close);
let search_icon = hyper8_icons::search(translations.search);
let t_browse = translations.browse;
let t_search = translations.search;
@@ -135,7 +137,7 @@ impl Layout {
navigation_script = format!(r#"<script defer src="{root_prefix}navigation.js?{navigation_js_asset_hash}"></script>"#);
let chevron_right_icon = icons::CHEVRON_RIGHT;
let chevron_right_icon = hyper8_icons::CHEVRON_RIGHT;
let navigation_templates = formatdoc!(r#"
<template id="chevron_right_icon">
{chevron_right_icon}
@@ -151,12 +153,14 @@ impl Layout {
let favicon_png_asset_hash = AssetHashes::FAVICON_PNG;
let site_css_asset_hash = asset_hashes.site_css.as_ref().unwrap();
let meta_generator = meta_generator();
let head = formatdoc!(r#"
<head>
<title>{title}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{META_GENERATOR}
{meta_generator}
{extra_meta}
<link href="{root_prefix}favicon.png?{favicon_png_asset_hash}" rel="icon" type="image/png">
<link rel="stylesheet" href="{root_prefix}site.css?{site_css_asset_hash}">
@@ -167,8 +171,8 @@ impl Layout {
</head>
"#);
let dark_icon = icons::dark(translations.dark_color_scheme);
let light_icon = icons::light(translations.light_color_scheme);
let dark_icon = hyper8_icons::dark(translations.dark_color_scheme);
let light_icon = hyper8_icons::light(translations.light_color_scheme);
let theme_toggle = match &site.theme.kind {
ThemeKind::Adaptive { .. } => formatdoc!(r#"
<div id="theme-toggles">
@@ -179,8 +183,6 @@ impl Layout {
ThemeKind::Static(_) => String::new()
};
let version_display = env!("HYPER8_VERSION_DISPLAY");
if self.clipboard_script {
templates.push_str(&copy_button_icon_templates(translations));
}
@@ -192,9 +194,9 @@ impl Layout {
let root_url = traversal.index_asc(site);
let site_path = traversal.site_path().normalized();
let key_icon = icons::KEY;
let logo_monochrome_icon = icons::logo_monochrome(None);
let logo_polychrome_icon = icons::logo_polychrome(Some("Hyper 8"));
let key_icon = hyper8_icons::KEY;
let logo_monochrome_icon = hyper8_icons::logo_monochrome(None);
let logo_polychrome_icon = hyper8_icons::logo_polychrome(Some("Hyper 8"));
let t_adjust_playback_speed_cursor_above_playback_speed_indicator = translations.adjust_playback_speed_cursor_above_playback_speed_indicator;
let t_arrow_left_right = translations.arrow_left_right;
let t_clear_search_or_close_browse_panel = translations.clear_search_or_close_browse_panel;
@@ -234,7 +236,7 @@ impl Layout {
{e_site_title}
</a>
<a href="https://simonrepp.com/hyper8" target="_blank">
{logo_monochrome_icon} Hyper 8 Video System {version_display}
{logo_monochrome_icon} Hyper 8 Video System {VERSION_WITHOUT_PATCH}
</a>
<button class="shortcuts">
{key_icon} {t_shortcuts}
@@ -6,18 +6,18 @@ use std::path::Path;
use indoc::formatdoc;
use hyper8_translations::Translations;
use crate::{
Asc,
AudioFile,
Desc,
Playlist,
Site,
Translations,
Video,
VideoFile
};
use crate::build::widgets::thumbnail;
use crate::icons;
use crate::util::{
casual_duration,
html_escape_inside_attribute,
@@ -83,7 +83,7 @@ pub fn player(
None => String::new()
};
let active_marker_icon = icons::active_marker(translations.active);
let active_marker_icon = hyper8_icons::active_marker(translations.active);
let subtitle_controls;
let subtitle_tracks;
@@ -126,7 +126,7 @@ pub fn player(
.collect::<Vec<String>>()
.join("\n");
let subtitles_icon = icons::subtitles(Some(translations.subtitles));
let subtitles_icon = hyper8_icons::subtitles(Some(translations.subtitles));
let t_no_subtitles = translations.no_subtitles;
subtitle_controls = formatdoc!(r#"
<div class="selector_control subtitles">
@@ -242,7 +242,7 @@ pub fn player(
);
number_context = format!(r#"<span class="number_context">{t_current_video_xxx_of_xxx_in_xxx}</span>"#);
replay_indicator = icons::replay(translations.replay);
replay_indicator = hyper8_icons::replay(translations.replay);
video_iterated_prefix = video_prefix.to_string();
} else {
current = "";
@@ -297,7 +297,7 @@ pub fn player(
</div>
"#)
} else {
let replay_icon = icons::replay(translations.replay);
let replay_icon = hyper8_icons::replay(translations.replay);
let t_replay = translations.replay;
formatdoc!(r#"
<div class="outro_video_context">
@@ -308,14 +308,14 @@ pub fn player(
"#)
};
let maximize_icon = icons::maximize(translations.maximize);
let volume_loud_icon = icons::volume_loud(translations.loud_volume);
let maximize_icon = hyper8_icons::maximize(translations.maximize);
let volume_loud_icon = hyper8_icons::volume_loud(translations.loud_volume);
let video_title = video.title_or_slug_or_generic_label(translations);
let video_title_escaped = html_escape_outside_attribute(video_title);
let embed_intro_extensions = if embed {
let link_icon = icons::link(Some(translations.external_link));
let link_icon = hyper8_icons::link(Some(translations.external_link));
let site_url = site.absolute_url_unchecked();
let video_url = match site.clean_urls {
true => video_prefix.to_string(),
@@ -339,7 +339,7 @@ pub fn player(
.collect::<Vec<String>>()
.join("\n");
let subtitles_icon = icons::subtitles(Some(translations.subtitles));
let subtitles_icon = hyper8_icons::subtitles(Some(translations.subtitles));
let t_no_subtitles = translations.no_subtitles;
formatdoc!(r#"
@@ -391,7 +391,7 @@ pub fn player(
let duration_formatted = precise_duration(duration);
let play_icon = icons::play(translations.play);
let play_icon = hyper8_icons::play(translations.play);
formatdoc!(r#"
<style>
:root {{ --video-aspect: {aspect_ratio}; }}
@@ -438,15 +438,15 @@ pub fn player(
/// these are injected to the end of the body in the regular page and embed
/// layouts.
pub fn player_icon_templates(translations: &Translations) -> String {
let maximize_icon = icons::maximize(translations.maximize);
let minimize_icon = icons::minimize(translations.minimize);
let pause_icon = icons::pause(translations.pause);
let play_icon = icons::play(translations.play);
let replay_icon = icons::replay(translations.replay);
let subtitles_icon = icons::subtitles(Some(translations.subtitles));
let volume_loud_icon = icons::volume_loud(translations.loud_volume);
let volume_medium_icon = icons::volume_medium(translations.medium_volume);
let volume_muted_icon = icons::volume_muted(translations.sound_muted);
let maximize_icon = hyper8_icons::maximize(translations.maximize);
let minimize_icon = hyper8_icons::minimize(translations.minimize);
let pause_icon = hyper8_icons::pause(translations.pause);
let play_icon = hyper8_icons::play(translations.play);
let replay_icon = hyper8_icons::replay(translations.replay);
let subtitles_icon = hyper8_icons::subtitles(Some(translations.subtitles));
let volume_loud_icon = hyper8_icons::volume_loud(translations.loud_volume);
let volume_medium_icon = hyper8_icons::volume_medium(translations.medium_volume);
let volume_muted_icon = hyper8_icons::volume_muted(translations.sound_muted);
formatdoc!(r#"
<template id="maximize_icon">
@@ -5,16 +5,16 @@ use std::fs;
use indoc::formatdoc;
use hyper8_translations::Translations;
use crate::{
Context,
Site,
SitePath,
Translations
SitePath
};
use crate::build::{AssetHashes, Layout};
use crate::build::outline::Traversal;
use crate::build::widgets::{copy_button, unlisted_badge};
use crate::icons;
use crate::util::html_escape_outside_attribute;
use super::feeds::{
@@ -84,7 +84,7 @@ pub fn subscribe_html(
let atom_feed_choice = feed_choice(
translations.atom_description,
ATOM_FILENAME,
icons::ATOM,
hyper8_icons::ATOM,
"Atom",
site,
site_path,
@@ -94,7 +94,7 @@ pub fn subscribe_html(
let media_rss_choice = feed_choice(
translations.media_rss_description,
MEDIA_RSS_FILENAME,
&icons::rss(None),
&hyper8_icons::rss(None),
"Media RSS",
site,
site_path,
@@ -104,7 +104,7 @@ pub fn subscribe_html(
let plain_rss_feed_choice = feed_choice(
translations.plain_rss_description,
PLAIN_RSS_FILENAME,
&icons::rss(None),
&hyper8_icons::rss(None),
translations.plain_rss,
site,
site_path,
@@ -114,7 +114,7 @@ pub fn subscribe_html(
let podcast_rss_choice = feed_choice(
translations.podcast_rss_description,
PODCAST_RSS_FILENAME,
&icons::rss(None),
&hyper8_icons::rss(None),
"Podcast RSS",
site,
site_path,
@@ -17,7 +17,6 @@ use crate::build::{AssetHashes, Layout};
use crate::build::outline::{Traversal, TraversalStep};
use crate::build::player::player;
use crate::build::widgets::{copy_button, links, unlisted_badge};
use crate::icons;
use crate::util::{
html_escape_outside_attribute,
localized_date_d_mmmm_yyyy
@@ -83,7 +82,7 @@ pub fn video_html(
}
if video.embedding() && site.base_url.is_some() && !video.video_files.is_empty() {
let embed_icon = icons::EMBED;
let embed_icon = hyper8_icons::EMBED;
let t_embed = &translations.embed;
let t_embed_codes_permalink = &translations.embed_codes_permalink;
@@ -100,7 +99,7 @@ pub fn video_html(
if video.download() && !video.video_files.is_empty() {
let t_download = translations.download;
let t_download_permalink = translations.download_permalink;
let download_icon = icons::DOWNLOAD;
let download_icon = hyper8_icons::DOWNLOAD;
let download = formatdoc!(r#"
<a href="{t_download_permalink}{index_suffix}">
@@ -113,7 +112,7 @@ pub fn video_html(
if let Some(playlist) = playlist {
let t_playlist = translations.playlist;
let playlist_icon = icons::playlist(None);
let playlist_icon = hyper8_icons::playlist(None);
let playlist_site_path = playlist.site_path.normalized();
let toggle_playlist = formatdoc!(r#"
<button data-site-path="{playlist_site_path}" id="toggle_playlist">
@@ -126,7 +125,7 @@ pub fn video_html(
if !video.subtitles.is_empty() {
let t_transcript = translations.transcript;
let subtitles_icon = icons::subtitles(None);
let subtitles_icon = hyper8_icons::subtitles(None);
let toggle_transcript = formatdoc!(r#"
<button id="toggle_transcript">
{subtitles_icon} {t_transcript}
@@ -6,15 +6,15 @@ use std::sync::Arc;
use indoc::formatdoc;
use hyper8_translations::Translations;
use crate::{
Banner,
Language,
Link,
Site,
Translations,
Video
};
use crate::icons;
use crate::util::{
casual_duration,
html_escape_outside_attribute
@@ -67,7 +67,7 @@ pub fn copy_button(
content_value: &str,
label: &str
) -> String {
let copy_icon = icons::COPY;
let copy_icon = hyper8_icons::COPY;
format!(r#"
<button data-{content_key}="{content_value}" data-copy>
<span class="icon">{copy_icon}</span>
@@ -80,9 +80,9 @@ pub fn copy_button(
/// these are injected to the end of the body in the regular page and embed
/// layouts.
pub fn copy_button_icon_templates(translations: &Translations) -> String {
let copy_icon = icons::COPY;
let failure_icon = icons::failure(translations.failed);
let success_icon = icons::success(translations.copied);
let copy_icon = hyper8_icons::COPY;
let failure_icon = hyper8_icons::failure(translations.failed);
let success_icon = hyper8_icons::success(translations.copied);
formatdoc!(r#"
<template id="copy_icon">
@@ -102,7 +102,7 @@ pub fn links(language: &Language, links: &[Link]) -> String {
return String::new();
}
let link_icon = icons::link(Some(language.translations.external_link));
let link_icon = hyper8_icons::link(Some(language.translations.external_link));
let items = links
.iter()
@@ -134,7 +134,7 @@ pub fn subscribe_button(
site: &Site
) -> String {
let index_suffix = site.index_suffix();
let feed_icon = icons::rss(Some(site.language.translations.feed));
let feed_icon = hyper8_icons::rss(Some(site.language.translations.feed));
let t_subscribe = site.language.translations.subscribe;
formatdoc!(r#"
View File
+2 -57
View File
@@ -9,15 +9,14 @@ use std::sync::Arc;
use chrono::NaiveDate;
use url::Url;
use hyper8_translations::Translations;
use crate::{
Asc,
Banner,
CollectionUpdate,
Comparison,
Container,
ContainerOrder,
Context,
Desc,
FileMeta,
GetAnyResult,
HyperDir,
@@ -29,8 +28,6 @@ use crate::{
PlaylistUpdate,
Site,
SitePath,
Translations,
TreeViewOrder,
Video,
VideoOrder,
VideoUpdate
@@ -515,36 +512,6 @@ impl Collection {
}
}
pub fn containers_in_tree_view_order(
&self,
language: &Language,
tree_view_order: &TreeViewOrder
) -> Vec<Container> {
let mut containers = Vec::new();
for playlist in &self.playlists {
containers.push(Container::Playlist(playlist.clone()));
}
for subcollection in &self.subcollections {
containers.push(Container::Collection(subcollection.clone()));
}
let (comparison, direction) = match tree_view_order {
TreeViewOrder::Default |
TreeViewOrder::SiteOrder =>
Container::container_order_comparison_and_direction(&self.container_order),
TreeViewOrder::TitleAsc =>
(Container::cmp_slug_then_title as Comparison<Container>, Asc),
TreeViewOrder::TitleDesc =>
(Container::cmp_slug_then_title as Comparison<Container>, Desc)
};
containers.sort_unstable_by(|a, b| comparison(a, b, &language.icu_collator, direction));
containers
}
/// When we convert between types (collection, playlist, video) we pass
/// over existing inherited options using this method.
pub fn copy_inherited_options(&self) -> InheritedOptions {
@@ -1571,28 +1538,6 @@ impl Collection {
}
}
pub fn videos_in_tree_view_order(
&self,
language: &Language,
tree_view_order: &TreeViewOrder
) -> Vec<Arc<Video>> {
let mut videos: Vec<Arc<Video>> = self.videos.clone();
let (comparison, direction) = match tree_view_order {
TreeViewOrder::Default |
TreeViewOrder::SiteOrder =>
Video::video_order_comparison_and_direction(&self.video_order),
TreeViewOrder::TitleAsc =>
(Video::cmp_slug_then_title as Comparison<Video>, Asc),
TreeViewOrder::TitleDesc =>
(Video::cmp_slug_then_title as Comparison<Video>, Desc)
};
videos.sort_unstable_by(|a, b| comparison(a, b, &language.icu_collator, direction));
videos
}
pub fn write_manifest(&self, context: &Context) {
let mut eno = String::new();
+15 -35
View File
@@ -26,8 +26,6 @@ use crate::{
Site,
SiteContent,
SitePath,
Theme,
TreeViewOrder,
Video,
Worker
};
@@ -43,9 +41,6 @@ pub struct Context {
pub cache_dir: PathBuf,
pub completed_jobs: Mutex<Vec<CompletedJob>>,
pub deploying: Mutex<DeploymentState>,
pub editor_language: Mutex<Language>,
pub editor_theme: Mutex<Theme>,
pub editor_tree_view_order: Mutex<TreeViewOrder>,
pub job_queue: Mutex<Vec<QueuedJob>>,
pub site: Mutex<Arc<Site>>,
pub site_dir: PathBuf,
@@ -253,7 +248,11 @@ impl Context {
/// empty, the empty site itself) and convert it to a collection. This
/// may fail if we're converting from a video that is inside a playlist,
/// as a playlist can not contain a collection.
pub fn convert_to_collection(&self, site_path: &SitePath) -> Result<Arc<Collection>, String> {
pub fn convert_to_collection(
&self,
language: &Language,
site_path: &SitePath
) -> Result<Arc<Collection>, String> {
let mut site_mut = self.get_site_mut();
if site_path.is_root_path() {
@@ -369,7 +368,6 @@ impl Context {
match self.get_any(site_path) {
GetAnyResult::Collection(collection) => Ok(collection),
GetAnyResult::None => {
let language = self.get_editor_language();
Err(language.translations.the_resource_xxx_was_not_found_message(site_path.normalized()))
}
GetAnyResult::Playlist(playlist) => {
@@ -469,7 +467,11 @@ impl Context {
/// playlist can not contain), or if we're converting from a video that
/// is contained inside a playlist (as a playlist can not contain another
/// playlist).
pub fn convert_to_playlist(&self, site_path: &SitePath) -> Result<Arc<Playlist>, String> {
pub fn convert_to_playlist(
&self,
language: &Language,
site_path: &SitePath
) -> Result<Arc<Playlist>, String> {
let mut site_mut = self.get_site_mut();
if site_path.is_root_path() {
@@ -631,7 +633,6 @@ impl Context {
Ok(playlist)
}
GetAnyResult::None => {
let language = self.get_editor_language();
Err(language.translations.the_resource_xxx_was_not_found_message(site_path.normalized()))
}
GetAnyResult::Playlist(playlist) => Ok(playlist),
@@ -693,7 +694,11 @@ impl Context {
/// Take the collection or playlist at the given path (or if the site is still empty,
/// the empty site itself) and convert it to a video. This only succeeds if we're
/// converting from a collection or playlist that contains exactly one video (or none).
pub fn convert_to_video(&self, site_path: &SitePath) -> Result<Arc<Video>, String> {
pub fn convert_to_video(
&self,
language: &Language,
site_path: &SitePath
) -> Result<Arc<Video>, String> {
let mut site_mut = self.get_site_mut();
if site_path.is_root_path() {
@@ -990,7 +995,6 @@ impl Context {
Ok(video)
}
GetAnyResult::None => {
let language = self.get_editor_language();
Err(language.translations.the_resource_xxx_was_not_found_message(site_path.normalized()))
}
GetAnyResult::Playlist(playlist) => {
@@ -1212,24 +1216,6 @@ impl Context {
self.site.lock().unwrap().get_collection(site_path)
}
/// Returns cloned `Language`, allowing subsequent read-only usage
/// of the language without keeping a lock on it.
pub fn get_editor_language(&self) -> Language {
self.editor_language.lock().unwrap().clone()
}
/// Returns cloned `TreeViewOrder`, allowing subsequent read-only usage
/// of the tree view order without keeping a lock on it.
pub fn get_editor_tree_view_order(&self) -> TreeViewOrder {
self.editor_tree_view_order.lock().unwrap().clone()
}
/// Returns cloned `Theme`, allowing subsequent read-only usage
/// of the theme without keeping a lock on it.
pub fn get_editor_theme(&self) -> Theme {
self.editor_theme.lock().unwrap().clone()
}
/// Returns the playlist at the given path.
pub fn get_playlist(&self, site_path: &SitePath) -> Option<Arc<Playlist>> {
self.site.lock().unwrap().get_playlist(site_path)
@@ -1258,9 +1244,6 @@ impl Context {
build_dir: PathBuf,
cache: Mutex<Cache>,
cache_dir: PathBuf,
editor_language: &str,
editor_theme: &str,
editor_tree_view_order: TreeViewOrder,
site: Arc<Site>,
site_dir: PathBuf
) -> Context {
@@ -1270,9 +1253,6 @@ impl Context {
cache_dir,
completed_jobs: Mutex::new(Vec::new()),
deploying: Mutex::new(DeploymentState::new()),
editor_language: Mutex::new(Language::from_code(editor_language)),
editor_theme: Mutex::new(Theme::from_key(editor_theme).unwrap_or_else(|_| Theme::default())),
editor_tree_view_order: Mutex::new(editor_tree_view_order),
job_queue: Mutex::new(Vec::new()),
site: Mutex::new(site),
site_dir
+4 -68
View File
@@ -1,12 +1,12 @@
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::io;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, SystemTime};
use std::time::SystemTime;
use crate::{Context, Site, Translations};
use hyper8_translations::Translations;
use crate::{Context, Site};
mod ftp;
mod ftp_config;
@@ -86,70 +86,6 @@ pub fn config_probe(
panic!("Caller must ensure that a complete FTP or rsync config is available")
}
/// Performs deployment in a synchronous fashion, reporting back all messages
/// and errors to the terminal. Also asks for explicit ("opt-in") confirmation
/// before performing the actual deployment.
pub fn perform_deploy_blocking_cli(
context_ref: &Arc<Context>,
site: &Site,
translations: &Translations
) -> Result<(), String> {
if site.base_url.is_none() {
let site_manifest_path = context_ref.site_dir.join("site.eno");
let message = translations.public_address_must_be_configured_for_deployment_text_cli(&site_manifest_path);
return Err(message);
}
if !site.complete_deployment_config() {
let message = translations.deployment_configuration_is_incomplete_can_not_deploy.to_string();
return Err(message);
}
let t_really_deploy = translations.really_deploy;
let base_url = &site.base_url.as_ref().unwrap().representation;
println!("{t_really_deploy} ({base_url})");
println!("{}", translations.type_y_and_press_enter_to_confirm_anything_else_will_abort);
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(_) => {
if input.trim() != "y" {
return Ok(());
}
}
Err(err) => return Err(err.to_string())
}
if let Err(err) = config_probe(context_ref, site, translations) {
let t_deployment_configuration_could_not_be_verified = translations.deployment_configuration_could_not_be_verified;
let message = format!("{t_deployment_configuration_could_not_be_verified} ({err})");
return Err(message);
}
start_deploy(context_ref, site, translations)?;
let mut seen_feedback = 0;
loop {
thread::sleep(Duration::from_secs(1));
if let Ok(deployment) = context_ref.deploying.lock() {
let feedback = &deployment.feedback;
if feedback.len() > seen_feedback {
eprintln!("{}", &feedback[seen_feedback..]);
seen_feedback = feedback.len();
}
if !deployment.active() {
break;
}
}
}
Ok(())
}
/// Caller needs to ensure that there is either a complete FTP or rsync
/// configuration available, otherwise will panic.
pub fn start_deploy(
+3 -1
View File
@@ -12,7 +12,9 @@ use std::thread;
use chrono::{DateTime, Utc};
use indoc::indoc;
use crate::{Context, Site, SitePath, Translations};
use hyper8_translations::Translations;
use crate::{Context, Site, SitePath};
use crate::util::{
nanoid,
string_from_os_string
@@ -10,7 +10,9 @@ use std::thread;
use indoc::indoc;
use crate::{Context, Site, SitePath, Translations};
use hyper8_translations::Translations;
use crate::{Context, Site, SitePath};
use crate::util::nanoid;
const RSYNC_CONFIG_PROBE_FILE_NAME: &str = ".hyper8_rsync_config_probe.txt";
@@ -179,8 +181,6 @@ pub fn start_deploy(
}
}
let context = Arc::clone(context_ref);
let remote_deploy_path = format!(
"{user}@{server}:{path}",
path = Path::new(site.ssh_config.path_unchecked()).join("./").to_string_lossy(),
@@ -192,11 +192,13 @@ pub fn start_deploy(
command.arg("-avP");
command.arg("--delete");
command.arg(context.build_dir.join("./"));
command.arg(context_ref.build_dir.join("./"));
command.arg(remote_deploy_path);
command.stdout(Stdio::piped());
let context = Arc::clone(context_ref);
thread::spawn(move || {
match command.spawn() {
Ok(mut child) => {
+1 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 Simon Repp
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::fs;
View File
+26 -11
View File
@@ -5,6 +5,8 @@ use std::sync::Arc;
use std::thread;
use std::time::Duration;
use hyper8_translations::Translations;
use crate::Context;
mod encode;
@@ -21,18 +23,31 @@ pub use worker::Worker;
use encode::FFmpegEncodingStats;
pub fn start_workers(count: usize, context_ref: &Arc<Context>) {
// TODO: This currently "bakes" the language into the worker, i.e. whatever the
// passed language when the worker is started, remains the language in which
// it produces messages throughout the session (no matter if the language has
// then changed). I.e. we need to move the passing of language closer to the
// invocation (e.g. passing it in the Job/JobDesc as a next step, or giving
// a ref-counted language handle to the worker that it shares with context
// itself). (In an ideal scenario the language would only be brought into
// the equation at display time (i.e. all errors are just semantic markers
// that are then rendered into the given language when they are displayed),
// but this can be done someday (TM) because it's so complex and has little
// reward in 99% of real life cases).
pub fn start_workers(
context_ref: &Arc<Context>,
count: usize,
translations: &Translations
) {
for worker_number in 1..=count {
let context = Arc::clone(context_ref);
let translations = translations.clone();
thread::spawn(move || {
let high_priority_only = worker_number == 1;
let worker = Worker::new(high_priority_only, worker_number);
let language = context.get_editor_language();
let translations = &language.translations;
loop {
let job = match context.take_next_job(&worker) {
Some(job) => job,
@@ -48,14 +63,14 @@ pub fn start_workers(count: usize, context_ref: &Arc<Context>) {
format,
&context,
job.id.clone(),
translations,
&translations,
video
).err()
}
JobDesc::AudioMeta { video } => {
Worker::compute_audio_metadata(
&context,
translations,
&translations,
video
).err()
}
@@ -63,21 +78,21 @@ pub fn start_workers(count: usize, context_ref: &Arc<Context>) {
Worker::compute_collection_fast_tasks(
collection,
&context,
translations
&translations
).err()
}
JobDesc::PlaylistFastTasks { playlist } => {
Worker::compute_playlist_fast_tasks(
&context,
playlist,
translations
&translations
).err()
}
JobDesc::VideoFastTasks { poster_aspect, video } => {
Worker::compute_video_fast_tasks(
&context,
poster_aspect,
translations,
&translations,
video
).err()
}
@@ -85,7 +100,7 @@ pub fn start_workers(count: usize, context_ref: &Arc<Context>) {
Worker::render_video_format(
&context,
job.id.clone(),
translations,
&translations,
video,
format
).err()
@@ -95,7 +110,7 @@ pub fn start_workers(count: usize, context_ref: &Arc<Context>) {
&context,
poster_aspect,
timecode,
translations,
&translations,
video
).err()
}
@@ -8,6 +8,8 @@ use std::sync::Arc;
use std::thread;
use std::time::Instant;
use hyper8_translations::Translations;
use crate::{
AudioFile,
AudioFormat,
@@ -16,7 +18,6 @@ use crate::{
EncodingApproach,
FileMeta,
JobProgress,
Translations,
Video,
VideoFile,
VideoFormat
@@ -3,15 +3,15 @@
use std::sync::Arc;
use hyper8_translations::Translations;
use crate::{
AudioFormat,
Collection,
Playlist,
Translations,
Video,
VideoFormat
};
use crate::editor::routes;
/// The job description specifies what needs to be done in a processing job,
/// and to which resource (collection, playlist or video) it applies.
@@ -105,26 +105,6 @@ impl JobDesc {
}
}
}
/// The url for editing the collection, playlist or video which is part of
/// this job description in the editor.
pub fn target_edit_url(&self) -> String {
match self {
JobDesc::CollectionFastTasks { collection } => {
routes::collection_edit(collection)
}
JobDesc::PlaylistFastTasks { playlist } => {
routes::playlist_edit(playlist)
}
JobDesc::AudioFormat { video, .. } |
JobDesc::AudioMeta { video } |
JobDesc::VideoFastTasks { video, .. } |
JobDesc::VideoFormat { video, .. } |
JobDesc::VideoPickPoster { video, .. } => {
routes::video_edit(video)
}
}
}
}
impl PartialEq for JobDesc {
+3 -5
View File
@@ -7,11 +7,9 @@ use std::process::{Command};
use serde_derive::Deserialize;
use crate::{
AudioMeta,
Translations,
VideoMeta
};
use hyper8_translations::Translations;
use crate::{AudioMeta, VideoMeta};
pub const FFPROBE: &str = if cfg!(windows) { "ffprobe.exe" } else { "ffprobe" };
@@ -4,6 +4,8 @@
use std::fs;
use std::sync::Arc;
use hyper8_translations::Translations;
use crate::{
AudioFormat,
AudioMetaCached,
@@ -21,7 +23,6 @@ use crate::{
PosterFile,
PosterSourceCached,
ResizeMode,
Translations,
Video,
VideoFormat,
VideoMetaCached,
+1 -1
View File
@@ -8,7 +8,7 @@ use icu_locale::Locale as IcuLocale;
use icu_locale::locale as icu_locale;
use log::warn;
use crate::Translations;
use hyper8_translations::Translations;
#[derive(Debug)]
pub struct Language {
+106
View File
@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
mod aspect_ratio;
mod audio_file;
mod audio_format;
mod audio_meta;
mod banner;
mod build;
mod cache;
mod collection;
mod container;
mod container_order;
mod context;
mod deploy;
mod file_meta;
mod font;
mod hyper_dir;
mod image_processor;
mod inherited_options;
mod jobs;
mod language;
mod link;
mod lqip;
mod manifest;
mod markdown_subset;
mod platform_integration;
mod playlist;
mod poster_assets;
mod poster_file;
mod poster_meta;
mod preview_server;
mod site;
mod site_path;
mod site_url;
mod sort;
mod subtitles;
mod theme;
mod video;
mod video_file;
mod video_format;
mod video_meta;
mod video_order;
pub mod browser_support;
pub mod util;
pub mod watcher;
use audio_meta::AudioMeta;
use cache::{AudioMetaCached, PosterAssetsCached, PosterSourceCached, VideoMetaCached};
use deploy::{FtpConfig, SshConfig};
use image_processor::{ImageProcessor, ResizeMode};
use inherited_options::InheritedOptions;
use jobs::{CompletedJob, QueuedJob, Worker};
use lqip::Lqip;
use markdown_subset::MarkdownSubset;
use platform_integration::{PlatformIntegrationImage, PlatformIntegrationMeta};
use poster_assets::{PosterAsset, PosterAssets};
use poster_meta::PosterMeta;
use sort::SortDirection;
use video_format::{Codec, EncodingApproach};
use video_meta::VideoMeta;
pub use aspect_ratio::AspectRatio;
pub use audio_file::AudioFile;
pub use audio_format::AudioFormat;
pub use banner::Banner;
pub use build::perform_build;
pub use cache::Cache;
pub use collection::Collection;
pub use container::Container;
pub use container_order::ContainerOrder;
pub use context::{
CollectionUpdate,
Context,
GetAnyResult,
PlaylistUpdate,
SiteUpdate,
VideoParent,
VideoUpdate
};
pub use deploy::DeploymentState;
// TODO: Rename "config_probe" to something clearer? (e.g. test_deployment_config or such)
pub use deploy::{config_probe, start_deploy};
pub use file_meta::FileMeta;
pub use font::Font;
pub use hyper_dir::HyperDir;
pub use jobs::{JobDesc, JobProgress};
pub use jobs::start_workers;
pub use language::Language;
pub use link::Link;
pub use manifest::MAX_SYNOPSIS_CHARS;
pub use platform_integration::PlatformIntegration;
pub use playlist::Playlist;
pub use poster_file::PosterFile;
pub use preview_server::start as start_preview_server;
pub use site::{Site, SiteContent};
pub use site_path::SitePath;
pub use site_url::SiteUrl;
pub use sort::{Asc, Comparison, Desc};
pub use subtitles::SubtitleFile;
pub use theme::{Theme, ThemeKind};
pub use video::Video;
pub use video_file::VideoFile;
pub use video_format::VideoFormat;
pub use video_order::VideoOrder;
View File
View File
@@ -9,7 +9,9 @@
//! - https://ogp.me/
//! - https://developers.facebook.com/docs/sharing/webmasters/#video
use crate::{Site, Translations};
use hyper8_translations::Translations;
use crate::Site;
use crate::util::html_escape_inside_attribute;
pub struct Embed {
@@ -137,7 +139,7 @@ impl PlatformIntegrationMeta {
// Open Graph metadata (for link previews and content embeds)
// TODO: Sketched out for usage when we have audio(-only) content too
// TODO: Uncomment and put the new audio(-only) content to use here
// if let Some(audio) = &self.audio {
// tags.push(format!(r#"<meta property="og:audio" content="{audio}"/>"#));
// }
+2 -32
View File
@@ -9,12 +9,11 @@ use std::sync::Arc;
use chrono::NaiveDate;
use url::Url;
use hyper8_translations::Translations;
use crate::{
Asc,
Banner,
Comparison,
Context,
Desc,
FileMeta,
GetAnyResult,
HyperDir,
@@ -24,8 +23,6 @@ use crate::{
PlatformIntegration,
Site,
SitePath,
Translations,
TreeViewOrder,
Video,
VideoOrder,
VideoUpdate
@@ -91,14 +88,6 @@ impl Playlist {
fn apply_manifest(&mut self, context: &Context, manifest_path: &Path) {
let content = fs::read_to_string(manifest_path).unwrap();
// TODO: Tricky manifest problem/question in general: If there is anything wrong
// with the manifest (especially: syntax error rendering the entire manifest useless),
// we can potentially erase everything contained in it by doing an update via the browser
// editor (it persists what has been read - nothing!). Consequently we should e.g. disable
// the edit form/updates via browser editor while there are issues with the manifest, or
// try to read in some things in a "broken state", e.g. registering a poster although the
// image referenced was not found.
let document = match enolib::parse(&content) {
Ok(document) => document,
Err(err) => {
@@ -1070,25 +1059,6 @@ impl Playlist {
}
}
pub fn videos_in_tree_view_order(
&self,
language: &Language,
tree_view_order: &TreeViewOrder
) -> Vec<Arc<Video>> {
let mut videos: Vec<Arc<Video>> = self.videos.clone();
let (comparison, direction) = match tree_view_order {
TreeViewOrder::Default |
TreeViewOrder::SiteOrder => Video::video_order_comparison_and_direction(&self.order),
TreeViewOrder::TitleAsc => (Video::cmp_slug_then_title as Comparison<Video>, Asc),
TreeViewOrder::TitleDesc => (Video::cmp_slug_then_title as Comparison<Video>, Desc)
};
videos.sort_unstable_by(|a, b| comparison(a, b, &language.icu_collator, direction));
videos
}
pub fn write_manifest(&self, context: &Context) {
let mut eno = String::new();
@@ -19,8 +19,8 @@ const MAX_PORT_ATTEMPTS: u16 = 10;
#[actix_web::main]
pub async fn start(
build_dir: &Path,
ip_requested: &Option<IpAddr>,
port_requested: &Option<u16>
ip_requested: Option<&IpAddr>,
port_requested: Option<&u16>
) -> Result<(), ()> {
let bind_server = |build_dir_moving: PathBuf, ip: IpAddr, port: u16| {
HttpServer::new(move || {
@@ -35,10 +35,10 @@ pub async fn start(
.bind((ip, port))
};
let ip = ip_requested.unwrap_or(DEFAULT_PREVIEW_IP);
let ip = ip_requested.unwrap_or(&DEFAULT_PREVIEW_IP);
let (server, port_bound) = if let Some(port) = port_requested {
match bind_server(build_dir.to_path_buf(), ip, *port) {
match bind_server(build_dir.to_path_buf(), *ip, *port) {
Ok(server) => (server, *port),
Err(err) => {
eprintln!("Could not bind preview server to {ip}:{port} ({err})");
@@ -49,7 +49,7 @@ pub async fn start(
let mut port = DEFAULT_PREVIEW_PORT;
loop {
match bind_server(build_dir.to_path_buf(), ip, port) {
match bind_server(build_dir.to_path_buf(), *ip, port) {
Ok(server) => break (server, port),
Err(err) => {
if port > DEFAULT_PREVIEW_PORT + MAX_PORT_ATTEMPTS {
View File
View File
+2 -2
View File
@@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: 2024 Simon Repp
// SPDX-FileCopyrightText: 2024-2025 Simon Repp
// SPDX-License-Identifier: AGPL-3.0-or-later
use indoc::formatdoc;
use crate::Translations;
use hyper8_translations::Translations;
mod color;
mod dark_cool;

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