Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b176d023e | ||
|
|
eef929c065 | ||
|
|
91ea263b0c | ||
|
|
196e1c4615 | ||
|
|
8f321f06d0 | ||
|
|
0b338f3886 | ||
|
|
de76fc6c36 | ||
|
|
9fb0ca2dab | ||
|
|
42965dfcfa | ||
|
|
8c7b358534 | ||
|
|
d8851abf24 | ||
|
|
2c8254e50f | ||
|
|
15348f7b0e | ||
|
|
6071dda4f9 | ||
|
|
1f6416856a | ||
|
|
1370ab63d9 | ||
|
|
1e0f31ed8d | ||
|
|
fd4f284af6 | ||
|
|
51a114a699 | ||
|
|
75621ea85a | ||
|
|
e003e94e54 | ||
|
|
0d1bd6b014 | ||
|
|
9861263b08 | ||
|
|
9fa73c6e06 | ||
|
|
caada00cd9 | ||
|
|
df8d999ed7 | ||
|
|
3f94e48f12 | ||
|
|
68e57bd601 | ||
|
|
e65011cd51 | ||
|
|
b2f989b0b7 | ||
|
|
233d61d7a1 | ||
|
|
ab412c6f10 | ||
|
|
9d6b31200e | ||
|
|
a1aacad7dd | ||
|
|
6436de9f44 | ||
|
|
116782e6ed | ||
|
|
431f2efcf0 | ||
|
|
7c64da88da | ||
|
|
93eb78da91 | ||
|
|
f96e79ff49 | ||
|
|
39e6f4511f | ||
|
|
4c0d062d76 | ||
|
|
7da40d5177 | ||
|
|
f06b5098e2 | ||
|
|
eac503af46 | ||
|
|
16932374f5 | ||
|
|
35940a4a63 | ||
|
|
55543b3735 | ||
|
|
3d44e6e29e | ||
|
|
3244b802fa | ||
|
|
5e7a5d2cff | ||
|
|
ecb73e4f80 | ||
|
|
39092fa866 | ||
|
|
ca80275d28 | ||
|
|
8c192b17f1 | ||
|
|
40da6f6500 | ||
|
|
2a3982aa5a | ||
|
|
a1c2b4f5c8 | ||
|
|
213d3f7bd2 | ||
|
|
0788e98b54 | ||
|
|
65c8ddfa91 | ||
|
|
a60e20c0b4 | ||
|
|
54d417f4c5 | ||
|
|
98a74e3a96 | ||
|
|
b537223ffa | ||
|
|
8001f24195 | ||
|
|
778a4e72c8 | ||
|
|
083680232c | ||
|
|
b1b44ceb60 | ||
|
|
d988acbf6e | ||
|
|
6e05b7a1b5 | ||
|
|
8a364605cf | ||
|
|
30a8e72efe | ||
|
|
ff1a83898b | ||
|
|
f1b9299e46 | ||
|
|
2eefaaf7a1 | ||
|
|
a21b4a4275 | ||
|
|
e4c561eb41 | ||
|
|
420a23d359 | ||
|
|
9cd3b493c6 | ||
|
|
bbd5953000 | ||
|
|
b346750968 | ||
|
|
e5749f0e77 | ||
|
|
826cf00cbb | ||
|
|
6b7d034068 | ||
|
|
7bd3f6f8c6 | ||
|
|
0c550fcb1a | ||
|
|
3b2b7ede18 | ||
|
|
782b68e9a2 | ||
|
|
5c23568646 | ||
|
|
43e781571f | ||
|
|
84361eb93e | ||
|
|
38ab84d52e | ||
|
|
82e87fa883 | ||
|
|
de5daa3f92 | ||
|
|
fc0fba7573 | ||
|
|
b199738ee0 | ||
|
|
aa76113e8c | ||
|
|
bfe02a7880 | ||
|
|
ac81de6a48 | ||
|
|
2c2a63e80b | ||
|
|
3521f73142 | ||
|
|
88fd732eec | ||
|
|
490915a3e7 | ||
|
|
1802b0939e | ||
|
|
36b6aa78f8 | ||
|
|
499c07548c | ||
|
|
65580ec502 | ||
|
|
5d5b782760 | ||
|
|
09d0ae263f | ||
|
|
c3509d770f | ||
|
|
ee1043f92c | ||
|
|
e88b6ce39b | ||
|
|
0e535e52ad | ||
|
|
aa594b773e | ||
|
|
ac209f8f4f | ||
|
|
66abfd18ba | ||
|
|
79cf730270 | ||
|
|
698351f2a3 | ||
|
|
fc4eaff7f6 | ||
|
|
dadf14dbf4 | ||
|
|
ff695d7af6 | ||
|
|
a5e9bf2262 | ||
|
|
b49da80762 | ||
|
|
a0db62f0fd | ||
|
|
ebc0f1ae42 | ||
|
|
dc5f5c5906 | ||
|
|
98b7042192 | ||
|
|
76db001f59 | ||
|
|
60637394d6 | ||
|
|
f3b5a77475 | ||
|
|
d3e1d7fab9 | ||
|
|
e34f9cad68 | ||
|
|
37f29e2e48 | ||
|
|
f2a2aa6e28 | ||
|
|
76a639a045 | ||
|
|
4fd69c2439 | ||
|
|
64948e82a6 | ||
|
|
878763c987 | ||
|
|
959bdcd918 | ||
|
|
a9967f0e76 | ||
|
|
cd680ac319 | ||
|
|
3eebb7fec9 | ||
|
|
63753c878a | ||
|
|
769ee07d69 | ||
|
|
2fee3e12d5 | ||
|
|
5eb1e256e7 | ||
|
|
46b687aca2 | ||
|
|
df07e1f1e9 | ||
|
|
bb78fb389f | ||
|
|
5015cd30a1 | ||
|
|
33699a16ef | ||
|
|
9e54e9b864 | ||
|
|
d11c30c914 | ||
|
|
fe9dfbb63a | ||
|
|
aca784042e | ||
|
|
a2e14aaea3 | ||
|
|
ab97e73279 | ||
|
|
4313ccc980 | ||
|
|
397172e036 | ||
|
|
d7566dd7ab | ||
|
|
acf5852acc | ||
|
|
d5e2a222b5 | ||
|
|
06ca3be4f5 | ||
|
|
a4e7a785d7 | ||
|
|
dd8b03692a | ||
|
|
638f453cb8 | ||
|
|
5e5da8c95b | ||
|
|
e39a7f0360 | ||
|
|
5ea90154b1 | ||
|
|
f627da6d8f | ||
|
|
682f11c79e | ||
|
|
0bdbb5214b | ||
|
|
94f2d2ec68 | ||
|
|
384b841528 | ||
|
|
62f97317ff | ||
|
|
8210a9f063 | ||
|
|
b4d603098d | ||
|
|
ffd86f896c | ||
|
|
30b77afb23 | ||
|
|
0d879af3a4 | ||
|
|
4ac71a18c5 | ||
|
|
81e1fce6a0 | ||
|
|
855ddbdf2d | ||
|
|
0a7d3028c9 | ||
|
|
c5aebaffb9 | ||
|
|
931304887f | ||
|
|
3dcec8bc85 | ||
|
|
af30eea6f7 | ||
|
|
df7db6b35d | ||
|
|
d228a43baf | ||
|
|
ebcc52dd1a | ||
|
|
726206fd0c | ||
|
|
ac896fd93a | ||
|
|
85a4ff5c55 | ||
|
|
e3f15b0d93 | ||
|
|
e31a2c0a91 | ||
|
|
ea85abdc62 | ||
|
|
4f3c93635f | ||
|
|
82b9b90ff9 | ||
|
|
ad5ea26a81 | ||
|
|
6121d118f0 | ||
|
|
28ca7be96c | ||
|
|
a2a7f7d274 | ||
|
|
e1255a55a0 | ||
|
|
9f629ee5e2 | ||
|
|
6a82fcb6e0 | ||
|
|
35eac53b9d | ||
|
|
c84c36d774 | ||
|
|
3269b62e14 | ||
|
|
cd49fe0015 | ||
|
|
326fa8c4a4 | ||
|
|
51bdd4eff8 | ||
|
|
a64ca7d4ac | ||
|
|
ec3ac242b0 | ||
|
|
fbe7fa3c77 | ||
|
|
418d824c35 | ||
|
|
2e6823834f | ||
|
|
7ef20bc885 | ||
|
|
b031a68000 | ||
|
|
e4b648b55d | ||
|
|
d78ccbca25 | ||
|
|
bae6bc6863 | ||
|
|
faa43a7361 | ||
|
|
f8b3b708c0 | ||
|
|
4c40379470 | ||
|
|
0c32a89828 |
@@ -0,0 +1,63 @@
|
||||
name: 'publish'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- tauri-prod-release
|
||||
|
||||
# This workflow will trigger on each push to the `release` branch to create or update a GitHub release, build your app, and upload the artifacts to the release.
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: 'macos-latest' # for Arm based macs (M1 and above).
|
||||
args: '--target aarch64-apple-darwin'
|
||||
# - platform: 'macos-latest' # for Intel based macs.
|
||||
# args: '--target x86_64-apple-darwin'
|
||||
# - platform: 'ubuntu-22.04' # for Tauri v1 you could replace this with ubuntu-20.04.
|
||||
# args: ''
|
||||
# - platform: 'windows-latest'
|
||||
# args: ''
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
|
||||
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
|
||||
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
|
||||
|
||||
- name: install frontend dependencies
|
||||
run: cd ./crates/desktop/artcraft && yarn install # change this to npm, pnpm or bun depending on which one you use.
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
projectPath: ./crates/desktop/artcraft
|
||||
tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
|
||||
releaseName: 'App v__VERSION__'
|
||||
releaseBody: 'See the assets to download this version and install.'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="Rust">
|
||||
<option name="RIGHT_MARGIN" value="180" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="false" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "MySQL",
|
||||
"query": "\nUPDATE generic_inference_jobs\nSET\n status = \"complete_success\",\n on_success_result_entity_type = ?,\n on_success_result_entity_token = ?,\n failure_reason = NULL,\n internal_debugging_failure_reason = NULL,\n success_execution_millis = ?,\n success_inference_execution_millis = ?,\n retry_at = NULL,\n successfully_completed_at = NOW()\nWHERE token = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2bd7a5b886c9624ae48533ddac409cfca0668f30ab1f7016c412876b5904168b"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"files.exclude": {
|
||||
"**/*~": true,
|
||||
"frontend_old/**": true
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,7 @@ members = [
|
||||
"crates/cli/production/migration-tool",
|
||||
|
||||
# Desktop apps
|
||||
"crates/desktop/tauri-4o",
|
||||
"crates/desktop/tauri-realtime",
|
||||
"crates/desktop/artcraft",
|
||||
|
||||
# Shared libraries
|
||||
"crates/lib/actix_cors_configs",
|
||||
@@ -34,10 +33,12 @@ members = [
|
||||
"crates/lib/api_clients/google_drive_common",
|
||||
"crates/lib/api_clients/google_sign_in",
|
||||
"crates/lib/api_clients/openai_sora_client",
|
||||
"crates/lib/api_clients/storyteller_client",
|
||||
"crates/lib/api_clients/thumbnail_generator",
|
||||
"crates/lib/bootstrap",
|
||||
"crates/lib/bucket_paths",
|
||||
"crates/lib/caching/memory_caching",
|
||||
"crates/lib/caching/memory_store",
|
||||
"crates/lib/caching/redis_caching",
|
||||
"crates/lib/concurrency",
|
||||
"crates/lib/cookies",
|
||||
@@ -116,6 +117,7 @@ members = [
|
||||
"crates/vendor/candle/candle-transformers",
|
||||
"crates/vendor/candle/tensor-tools",
|
||||
"crates/vendor/limitation",
|
||||
"crates/vendor/tauri-plugin-http",
|
||||
# NB(bt,2023-12-12): Newrelic keeps breaking with upgrades. Kill for now.
|
||||
#"crates/vendor/newrelic-telemetry-sdk-rust",
|
||||
|
||||
@@ -146,13 +148,21 @@ publish = false
|
||||
# - tauri-runtime-wry
|
||||
# - tauri-utils
|
||||
#
|
||||
# Broken on Windows:
|
||||
# - core-foundation
|
||||
# - core-foundation-sys
|
||||
#
|
||||
workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
|
||||
|
||||
# External libs
|
||||
# Centrally pinned versions so that we can uniformly upgrade/downgrade versions across the workspace
|
||||
# If a crate needs to use an older/newer version, the dependency should be managed in that crate's own Cargo.toml
|
||||
anyhow = "1.0.97"
|
||||
base64 = "0.22.1"
|
||||
chrono = { version = "0.4.40", features = ["serde"] }
|
||||
cookie = "0.18.1"
|
||||
hex-literal = "1.0.0"
|
||||
indexmap = "2.9.0"
|
||||
log = "0.4.27"
|
||||
once_cell = "1.21.3"
|
||||
rand = "0.9.0"
|
||||
@@ -162,16 +172,26 @@ serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_derive = "1.0.219"
|
||||
serde_json = "1.0.140"
|
||||
serde_yaml = "0.9.1"
|
||||
thiserror = "2"
|
||||
toml = "0.7.4"
|
||||
url = "2.5.4"
|
||||
sha3 = "0.10.8"
|
||||
tauri = { version = "2.4.1", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution
|
||||
thiserror = "2.0.12"
|
||||
toml = "0.8.20"
|
||||
url = { version = "2.5.4", features = ["serde"] }
|
||||
|
||||
# Internal libs
|
||||
crockford = { path = "crates/lib/data/crockford" }
|
||||
enums = { path = "crates/_types/enums" }
|
||||
enums_public = { path = "crates/_types/enums_public" }
|
||||
errors = { path = "crates/lib/errors" }
|
||||
filesys = { path = "crates/lib/files/filesys" }
|
||||
idempotency = { path = "crates/lib/idempotency" }
|
||||
images = { path = "crates/lib/files/images" }
|
||||
memory_caching = { path = "crates/lib/caching/memory_caching" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
memory_store = { path = "crates/lib/caching/memory_store" }
|
||||
mimetypes = { path = "crates/lib/files/mimetypes" }
|
||||
openai_sora_client = { path = "crates/lib/api_clients/openai_sora_client" }
|
||||
primitives = { path = "crates/lib/data/primitives" }
|
||||
shared_service_components = { path = "crates/lib/shared_service_components" }
|
||||
storyteller_client = { path = "crates/lib/api_clients/storyteller_client" }
|
||||
subprocess_common = { path = "crates/lib/subprocess_common" }
|
||||
tokens = { path = "crates/_types/tokens" }
|
||||
|
||||
@@ -28,11 +28,12 @@ pub enum MediaFileOriginCategory {
|
||||
DeviceApi,
|
||||
|
||||
/// From Storyteller Studio Engine
|
||||
#[deprecated(note="This db field should only denote file provenance, not the product.")]
|
||||
#[serde(rename = "studio")]
|
||||
StorytellerStudio,
|
||||
|
||||
/// From Storyteller Studio Engine
|
||||
#[deprecated(note = "Use StorytellerStudio instead")]
|
||||
#[deprecated(note = "DO NOT USE. Originally deprecated in favor of `StorytellerStudio`.")]
|
||||
StoryEngine,
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ pub enum MediaFileOriginProductCategory {
|
||||
#[serde(rename = "vst")]
|
||||
VideoStyleTransfer,
|
||||
|
||||
/// Image Studio
|
||||
#[serde(rename = "image_studio")]
|
||||
ImageStudio,
|
||||
|
||||
/// Storyteller Studio
|
||||
#[serde(rename = "studio")]
|
||||
StorytellerStudio,
|
||||
@@ -87,6 +91,7 @@ impl MediaFileOriginProductCategory {
|
||||
Self::FaceFusion => "face_fusion",
|
||||
Self::FaceMirror => "face_mirror",
|
||||
Self::VideoStyleTransfer => "vst",
|
||||
Self::ImageStudio => "image_studio",
|
||||
Self::StorytellerStudio => "studio",
|
||||
Self::TextToSpeech => "tts",
|
||||
Self::VoiceConversion => "voice_conversion",
|
||||
@@ -105,6 +110,7 @@ impl MediaFileOriginProductCategory {
|
||||
"face_fusion" => Ok(Self::FaceFusion),
|
||||
"face_mirror" => Ok(Self::FaceMirror),
|
||||
"vst" => Ok(Self::VideoStyleTransfer),
|
||||
"image_studio" => Ok(Self::ImageStudio),
|
||||
"studio" => Ok(Self::StorytellerStudio),
|
||||
"tts" => Ok(Self::TextToSpeech),
|
||||
"voice_conversion" => Ok(Self::VoiceConversion),
|
||||
@@ -126,6 +132,7 @@ impl MediaFileOriginProductCategory {
|
||||
Self::FaceFusion,
|
||||
Self::FaceMirror,
|
||||
Self::VideoStyleTransfer,
|
||||
Self::ImageStudio,
|
||||
Self::StorytellerStudio,
|
||||
Self::TextToSpeech,
|
||||
Self::VoiceConversion,
|
||||
@@ -153,6 +160,7 @@ mod tests {
|
||||
assert_serialization(MediaFileOriginProductCategory::FaceFusion, "face_fusion");
|
||||
assert_serialization(MediaFileOriginProductCategory::FaceMirror, "face_mirror");
|
||||
assert_serialization(MediaFileOriginProductCategory::VideoStyleTransfer, "vst");
|
||||
assert_serialization(MediaFileOriginProductCategory::ImageStudio, "image_studio");
|
||||
assert_serialization(MediaFileOriginProductCategory::StorytellerStudio, "studio");
|
||||
assert_serialization(MediaFileOriginProductCategory::TextToSpeech, "tts");
|
||||
assert_serialization(MediaFileOriginProductCategory::VoiceConversion, "voice_conversion");
|
||||
@@ -170,6 +178,7 @@ mod tests {
|
||||
assert_eq!(MediaFileOriginProductCategory::FaceFusion.to_str(), "face_fusion");
|
||||
assert_eq!(MediaFileOriginProductCategory::FaceMirror.to_str(), "face_mirror");
|
||||
assert_eq!(MediaFileOriginProductCategory::VideoStyleTransfer.to_str(), "vst");
|
||||
assert_eq!(MediaFileOriginProductCategory::ImageStudio.to_str(), "image_studio");
|
||||
assert_eq!(MediaFileOriginProductCategory::StorytellerStudio.to_str(), "studio");
|
||||
assert_eq!(MediaFileOriginProductCategory::TextToSpeech.to_str(), "tts");
|
||||
assert_eq!(MediaFileOriginProductCategory::VoiceConversion.to_str(), "voice_conversion");
|
||||
@@ -187,6 +196,7 @@ mod tests {
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("face_fusion").unwrap(), MediaFileOriginProductCategory::FaceFusion);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("face_mirror").unwrap(), MediaFileOriginProductCategory::FaceMirror);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("vst").unwrap(), MediaFileOriginProductCategory::VideoStyleTransfer);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("image_studio").unwrap(), MediaFileOriginProductCategory::ImageStudio);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("studio").unwrap(), MediaFileOriginProductCategory::StorytellerStudio);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("tts").unwrap(), MediaFileOriginProductCategory::TextToSpeech);
|
||||
assert_eq!(MediaFileOriginProductCategory::from_str("voice_conversion").unwrap(), MediaFileOriginProductCategory::VoiceConversion);
|
||||
@@ -200,12 +210,13 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = MediaFileOriginProductCategory::all_variants();
|
||||
assert_eq!(variants.len(), 13);
|
||||
assert_eq!(variants.len(), 14);
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::Unknown));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceAnimator));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceFusion));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::FaceMirror));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VideoStyleTransfer));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::ImageStudio));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::StorytellerStudio));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::TextToSpeech));
|
||||
assert_eq!(variants.pop_first(), Some(MediaFileOriginProductCategory::VoiceConversion));
|
||||
|
||||
@@ -12,7 +12,7 @@ publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "app_lib"
|
||||
name = "artcraft_app_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
@@ -27,15 +27,16 @@ crockford = { workspace = true }
|
||||
errors = { workspace = true }
|
||||
filesys = { workspace = true }
|
||||
memory_caching = { workspace = true }
|
||||
memory_store = { workspace = true }
|
||||
openai_sora_client = { workspace = true }
|
||||
primitives = { workspace = true }
|
||||
storyteller_client = { workspace = true }
|
||||
subprocess_common = { workspace = true }
|
||||
tokens = { workspace = true }
|
||||
|
||||
# External
|
||||
#openssl = { version = "0.10", features = ["vendored"] }
|
||||
# NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
|
||||
anyhow = { workspace = true }
|
||||
base64 = "0.22.1"
|
||||
base64 = { workspace = true }
|
||||
bytes = "1.10"
|
||||
chrono = { workspace = true }
|
||||
const_format = "0.2.34"
|
||||
@@ -43,21 +44,31 @@ directories = "6.0.0"
|
||||
dirs = "6.0.0"
|
||||
futures = "0.3"
|
||||
image = "0.25.5"
|
||||
indexmap = { workspace = true }
|
||||
log = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["multipart", "stream"], default-features = false }
|
||||
reqwest = { workspace = true, features = ["multipart", "stream"], default-features = false } # NB(bt,2025-03-10): default-features = false on Windows while I debug openssl / rustls
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
tauri = { version = "2.4.1", features = ["unstable"] }
|
||||
tauri = { workspace = true }
|
||||
tauri-plugin-log = "2.3.1"
|
||||
tauri-plugin-upload = "2.2.1"
|
||||
tempdir = "0.3.7"
|
||||
tempfile = "3.18.0"
|
||||
tokio = { version = "1.42", features = ["rt", "rt-multi-thread", "fs"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
|
||||
# NB: We need to vendor this in so that we can access the cookies inside via the private API
|
||||
tauri-plugin-http = { path = "../../vendor/tauri-plugin-http" }
|
||||
|
||||
# NB: The version of the `discord-rich-presence` crate (0.2.5) last published has non-Send/Sync error
|
||||
# types and can't do threading. We need to wait for a new version to unpin from Git.
|
||||
discord-rich-presence = { git = "https://github.com/vionya/discord-rich-presence" }
|
||||
tauri-plugin-dialog = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
libloading = "0.8.6"
|
||||
serial_test = "3.2.0"
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "enables the default permissions",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"log:default",
|
||||
"upload:allow-download",
|
||||
"upload:default",
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "https://*.storyteller.ai"
|
||||
},
|
||||
{
|
||||
"url": "https://*.fakeyou.com"
|
||||
}
|
||||
],
|
||||
"deny": [
|
||||
{
|
||||
"url": "https://private.tauri.app"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 1023 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
@@ -0,0 +1,59 @@
|
||||
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use serde_derive::Serialize;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct GetAppPreferencesResponse {
|
||||
pub preferences: AppPreferencesPayload,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct AppPreferencesPayload {
|
||||
/// The downloads directory to use when a user downloads a file.
|
||||
pub preferred_download_directory: PreferredDownloadDirectory,
|
||||
|
||||
/// Play sounds on events.
|
||||
pub play_sounds: bool,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_success_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_failure_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_enqueue_sound: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_app_preferences_command(
|
||||
app_prefs: State<'_, AppPreferencesManager>,
|
||||
) -> Result<GetAppPreferencesResponse, String> {
|
||||
info!("get_app_preferences_command called");
|
||||
|
||||
let result = get_prefs(&app_prefs)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error getting app preferences: {:?}", err);
|
||||
format!("Error getting app preferences: {:?}", err)
|
||||
})?;
|
||||
|
||||
Ok(GetAppPreferencesResponse {
|
||||
preferences: result,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_prefs(app_prefs: &AppPreferencesManager) -> AnyhowResult<AppPreferencesPayload> {
|
||||
let prefs = app_prefs.get_clone()?;
|
||||
Ok(AppPreferencesPayload {
|
||||
preferred_download_directory: prefs.preferred_download_directory,
|
||||
play_sounds: prefs.play_sounds,
|
||||
generation_success_sound: prefs.generation_success_sound,
|
||||
generation_failure_sound: prefs.generation_failure_sound,
|
||||
generation_enqueue_sound: prefs.generation_enqueue_sound,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod get_app_preferences_command;
|
||||
pub mod update_app_preference_command;
|
||||
@@ -0,0 +1,116 @@
|
||||
use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager;
|
||||
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use openai_sora_client::sora_error::SoraError::AnyhowError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
/// For now, we'll only update a single preference at a time.
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateAppPreferencesRequest {
|
||||
pub preference: PreferenceName,
|
||||
/// We'll decode this with respect to the preference value.
|
||||
pub value: Option<ValueType>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum ValueType {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
DownloadDirectory(PreferredDownloadDirectory),
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PreferenceName {
|
||||
PreferredDownloadDirectory,
|
||||
PlaySounds,
|
||||
GenerationSuccessSound,
|
||||
GenerationFailureSound,
|
||||
GenerationEnqueueSound,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct UpdateAppPreferencesResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn update_app_preferences_command(
|
||||
request: UpdateAppPreferencesRequest,
|
||||
app_prefs: State<'_, AppPreferencesManager>,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
) -> Result<UpdateAppPreferencesResponse, String> {
|
||||
info!("update_app_preferences_command called");
|
||||
|
||||
update_prefs(request, &app_prefs, &app_data_root)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Error getting app preferences: {:?}", err);
|
||||
format!("Error getting app preferences: {:?}", err)
|
||||
})?;
|
||||
|
||||
Ok(UpdateAppPreferencesResponse {
|
||||
success: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_prefs(
|
||||
request: UpdateAppPreferencesRequest,
|
||||
app_prefs: &AppPreferencesManager,
|
||||
app_data_root: &AppDataRoot,
|
||||
) -> AnyhowResult<()> {
|
||||
let mut prefs = app_prefs.get_clone()?;
|
||||
|
||||
info!("Value is: {:?}", request.value);
|
||||
|
||||
match request.preference {
|
||||
PreferenceName::PreferredDownloadDirectory => {
|
||||
match request.value {
|
||||
Some(ValueType::DownloadDirectory(dir)) =>
|
||||
prefs.preferred_download_directory = dir,
|
||||
_ =>
|
||||
return Err(anyhow!("Invalid value: {:?}", request.value)),
|
||||
}
|
||||
}
|
||||
PreferenceName::PlaySounds => {
|
||||
match request.value {
|
||||
Some(ValueType::Bool(val)) =>
|
||||
prefs.play_sounds = val,
|
||||
_ =>
|
||||
return Err(anyhow!("Invalid value: {:?}", request.value)),
|
||||
}
|
||||
}
|
||||
PreferenceName::GenerationSuccessSound => {
|
||||
prefs.generation_success_sound = request.value
|
||||
.map(|val| string_value(&val))
|
||||
.transpose()?;
|
||||
}
|
||||
PreferenceName::GenerationFailureSound => {
|
||||
prefs.generation_failure_sound = request.value
|
||||
.map(|val| string_value(&val))
|
||||
.transpose()?;
|
||||
}
|
||||
PreferenceName::GenerationEnqueueSound => {
|
||||
prefs.generation_enqueue_sound = request.value
|
||||
.map(|val| string_value(&val))
|
||||
.transpose()?;
|
||||
}
|
||||
}
|
||||
|
||||
app_prefs.set_clone(&prefs)?;
|
||||
app_data_root.settings_dir().write_app_preferences(&prefs)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn string_value(value: &ValueType) -> AnyhowResult<String> {
|
||||
match value {
|
||||
ValueType::String(val) => Ok(val.to_string()),
|
||||
_ => Err(anyhow!("Invalid value type: {:?}", value)),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod app_preferences;
|
||||
pub mod flip_image;
|
||||
pub mod platform_info_command;
|
||||
pub mod sora;
|
||||
@@ -0,0 +1,63 @@
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::ImageReader;
|
||||
use log::info;
|
||||
use serde_derive::Serialize;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PlatformInfoResponse {
|
||||
pub os_platform: DetectedOs,
|
||||
pub webview_runtime: WebviewRuntime,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DetectedOs {
|
||||
Windows,
|
||||
#[serde(rename = "macos")]
|
||||
MacOS,
|
||||
Linux,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WebviewRuntime {
|
||||
#[serde(rename = "webkit_safari")]
|
||||
WebkitSafari,
|
||||
#[serde(rename = "webkit_gtk")]
|
||||
WebkitGtk,
|
||||
#[serde(rename = "edge_webview_2")]
|
||||
EdgeWebview2,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn platform_info_command() -> Result<PlatformInfoResponse, String> {
|
||||
info!("platform_info_command called...");
|
||||
|
||||
let os_platform = match OsPlatform::maybe_get() {
|
||||
Some(OsPlatform::Linux) => DetectedOs::Linux,
|
||||
Some(OsPlatform::MacOs) => DetectedOs::MacOS,
|
||||
Some(OsPlatform::Windows) => DetectedOs::Windows,
|
||||
None => DetectedOs::Unknown,
|
||||
};
|
||||
|
||||
// These are the webviews that Tauri uses on each OS:
|
||||
// https://tauri.app/reference/webview-versions/
|
||||
let webview_runtime = match os_platform {
|
||||
DetectedOs::Windows => WebviewRuntime::EdgeWebview2,
|
||||
DetectedOs::MacOS => WebviewRuntime::WebkitSafari,
|
||||
DetectedOs::Linux => WebviewRuntime::WebkitGtk,
|
||||
DetectedOs::Unknown => WebviewRuntime::Unknown,
|
||||
};
|
||||
|
||||
Ok(PlatformInfoResponse {
|
||||
os_platform,
|
||||
webview_runtime,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod open_sora_login_command;
|
||||
pub mod sora_image_generation_command;
|
||||
pub mod sora_image_remix_command;
|
||||
@@ -1,11 +1,11 @@
|
||||
use std::time::Duration;
|
||||
use crate::threads::sora_session_login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
use errors::AnyhowResult;
|
||||
use log::{error, info};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::time::Duration;
|
||||
use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowBuilder};
|
||||
use tokio::time::sleep_until;
|
||||
use errors::AnyhowResult;
|
||||
use crate::threads::login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL};
|
||||
use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies;
|
||||
|
||||
pub const START_URL_STR: &str = "https://storyteller.ai/";
|
||||
|
||||
@@ -20,7 +20,7 @@ pub static SORA_HOMEPAGE_URL : Lazy<Url> = Lazy::new(|| {
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_login_command(
|
||||
pub async fn open_sora_login_command(
|
||||
app: AppHandle,
|
||||
) -> Result<String, String> {
|
||||
info!("open_login_command called");
|
||||
@@ -0,0 +1,84 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use errors::AnyhowResult;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, ImageReader};
|
||||
use log::{error, info};
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
|
||||
use std::fs::read_to_string;
|
||||
use std::io::Cursor;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sora_image_generation_command(
|
||||
prompt: &str,
|
||||
image: Option<&str>,
|
||||
_app: AppHandle,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
sora_creds_holder: State<'_, SoraCredentialHolder>,
|
||||
) -> Result<String, String> {
|
||||
info!("image_generation_command called; processing image...");
|
||||
|
||||
generate_image(image, prompt, &app_data_root, &sora_creds_holder)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("error: {:?}", err);
|
||||
"there was an error".to_string()
|
||||
})?;
|
||||
|
||||
Ok("success".to_string())
|
||||
}
|
||||
|
||||
pub async fn generate_image(
|
||||
maybe_image: Option<&str>,
|
||||
prompt: &str,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_creds_holder: &SoraCredentialHolder,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let creds = read_sora_credentials_from_disk(app_data_root)
|
||||
.map_err(|err| {
|
||||
error!("Failed to read Sora credentials from disk: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
let mut sora_media_tokens = vec![];
|
||||
|
||||
if let Some(image) = maybe_image {
|
||||
let image_bytes = BASE64_STANDARD.decode(image)?;
|
||||
|
||||
let filename = "image.png".to_string();
|
||||
|
||||
let response = sora_media_upload_from_bytes(image_bytes, filename, CredentialMigrationRef::New(&creds))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upload image to Sora: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
sora_media_tokens.push(response.id);
|
||||
}
|
||||
|
||||
let response = sora_image_gen_remix(SoraImageGenRemixRequest {
|
||||
prompt: prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: CredentialMigrationRef::New(&creds),
|
||||
}).await
|
||||
.map_err(|err| {
|
||||
error!("Failed to call Sora image generation: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
println!(">> TASK ID: {:?} ", response.task_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::state::sora::read_sora_credentials_from_disk::read_sora_credentials_from_disk;
|
||||
use crate::state::sora::sora_credential_holder::SoraCredentialHolder;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::state::sora::sora_task_queue::SoraTaskQueue;
|
||||
use crate::utils::get_url_file_extension::get_url_file_extension;
|
||||
use crate::utils::simple_http_download::simple_http_download;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use base64::Engine;
|
||||
use errors::AnyhowResult;
|
||||
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
|
||||
use image::{DynamicImage, ImageReader};
|
||||
use log::{debug, error, info};
|
||||
use openai_sora_client::credentials::SoraCredentials;
|
||||
use openai_sora_client::creds::credential_migration::CredentialMigrationRef;
|
||||
use openai_sora_client::recipes::image_remix_with_session_auto_renew::{image_remix_with_session_auto_renew, ImageRemixAutoRenewRequest};
|
||||
use openai_sora_client::recipes::image_upload_from_file_with_session_auto_renew::{image_upload_from_file_with_session_auto_renew, ImageUploadFromFileAutoRenewRequest};
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use openai_sora_client::requests::image_gen::common::{ImageSize, NumImages};
|
||||
use openai_sora_client::requests::image_gen::sora_image_gen_remix::{sora_image_gen_remix, SoraImageGenRemixRequest};
|
||||
use openai_sora_client::requests::upload::upload_media_from_bytes::sora_media_upload_from_bytes;
|
||||
use openai_sora_client::requests::upload::upload_media_from_file::sora_media_upload_from_file;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::read_to_string;
|
||||
use std::io::Cursor;
|
||||
use storyteller_client::media_files::get_media_file::get_media_file;
|
||||
use storyteller_client::utils::api_host::ApiHost;
|
||||
use tauri::{AppHandle, Emitter, Manager, State};
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use crate::threads::sora_task_polling_thread::SoraImageGenerationComplete;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraImageEnqueueFailure {
|
||||
// TODO: Reason.
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SoraImageEnqueueSuccess {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SoraImageRemixCommand {
|
||||
/// Image media file; the engine or canvas snapshot (screenshot).
|
||||
pub snapshot_media_token: MediaFileToken,
|
||||
|
||||
/// The user's image generation prompt.
|
||||
pub prompt: String,
|
||||
|
||||
/// Turn off the system prompt.
|
||||
pub disable_system_prompt: Option<bool>,
|
||||
|
||||
/// Additional images to include (optional). Up to nine images.
|
||||
pub maybe_additional_images: Option<Vec<MediaFileToken>>,
|
||||
|
||||
pub maybe_number_of_samples: Option<u32>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sora_image_remix_command(
|
||||
app: AppHandle,
|
||||
request: SoraImageRemixCommand,
|
||||
app_data_root: State<'_, AppDataRoot>,
|
||||
sora_creds_manager: State<'_, SoraCredentialManager>,
|
||||
sora_task_queue: State<'_, SoraTaskQueue>,
|
||||
) -> Result<String, String> {
|
||||
|
||||
info!("image_generation_command called; scene media token: {:?}, additional images: {:?}",
|
||||
request.snapshot_media_token, request.maybe_additional_images);
|
||||
|
||||
// TODO(bt,2025-04-24): Better error messages to caller
|
||||
|
||||
let result = generate_image(request, &app_data_root, &sora_creds_manager, &sora_task_queue).await;
|
||||
|
||||
if let Err(err) = result {
|
||||
error!("error: {:?}", err);
|
||||
|
||||
let result = app.emit("sora-image-enqueue-failure", SoraImageEnqueueFailure{});
|
||||
if let Err(err) = result {
|
||||
error!("Failed to emit event: {:?}", err);
|
||||
}
|
||||
|
||||
return Err("there was an error".to_string());
|
||||
}
|
||||
|
||||
let result = app.emit("sora-image-enqueue-success", SoraImageEnqueueSuccess{});
|
||||
if let Err(err) = result {
|
||||
error!("Failed to emit event: {:?}", err);
|
||||
}
|
||||
|
||||
Ok("success".to_string())
|
||||
}
|
||||
|
||||
pub async fn generate_image(
|
||||
request: SoraImageRemixCommand,
|
||||
app_data_root: &AppDataRoot,
|
||||
sora_creds_manager: &SoraCredentialManager,
|
||||
sora_task_queue: &SoraTaskQueue,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
let response = get_media_file(&ApiHost::Storyteller, &request.snapshot_media_token).await?;
|
||||
|
||||
let media_file_url = &response.media_file.media_links.cdn_url;
|
||||
let extension_with_dot = get_url_file_extension(media_file_url)
|
||||
.map(|ext| format!(".{}", ext))
|
||||
.unwrap_or_else(|| ".png".to_string());
|
||||
|
||||
let filename = format!("{}{}", response.media_file.token.as_str(), extension_with_dot);
|
||||
let filename = app_data_root.downloads_dir().path().join(&filename);
|
||||
|
||||
simple_http_download(&media_file_url, &filename).await?;
|
||||
|
||||
let files_to_upload = vec![filename];
|
||||
|
||||
let mut creds = sora_creds_manager.get_credentials_required()?;
|
||||
|
||||
let credential_updated = maybe_upgrade_or_renew_session(&mut creds)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to upgrade or renew session: {:?}", err);
|
||||
err
|
||||
})?;
|
||||
|
||||
if credential_updated {
|
||||
info!("Storing updated credentials");
|
||||
sora_creds_manager.set_credentials(&creds)?;
|
||||
}
|
||||
|
||||
let mut sora_media_tokens = Vec::with_capacity(files_to_upload.len());
|
||||
|
||||
for (i, file_path) in files_to_upload.iter().enumerate() {
|
||||
info!("Uploading image {} of {}...", (i+1), files_to_upload.len());
|
||||
|
||||
let (response, maybe_new_credentials) =
|
||||
image_upload_from_file_with_session_auto_renew(ImageUploadFromFileAutoRenewRequest {
|
||||
file_path,
|
||||
credentials: &creds,
|
||||
}).await?;
|
||||
|
||||
if let Some(new_creds) = maybe_new_credentials {
|
||||
info!("Storing updated credentials.");
|
||||
sora_creds_manager.set_credentials(&new_creds)?;
|
||||
creds = new_creds;
|
||||
}
|
||||
|
||||
sora_media_tokens.push(response.id);
|
||||
}
|
||||
|
||||
info!("Calling image generation...");
|
||||
|
||||
// TODO(bt,2025-04-21): Download media tokens.
|
||||
// Note: This is incredibly inefficient. We should keep a local cache.
|
||||
// Also, if they've already been uploaded to OpenAI, we shouldn't continue to re-upload.
|
||||
|
||||
let (response, maybe_new_creds) =
|
||||
image_remix_with_session_auto_renew(ImageRemixAutoRenewRequest {
|
||||
prompt: request.prompt.to_string(),
|
||||
num_images: NumImages::One,
|
||||
image_size: ImageSize::Square,
|
||||
sora_media_tokens: sora_media_tokens.clone(),
|
||||
credentials: &creds,
|
||||
}).await?;
|
||||
|
||||
if let Some(new_creds) = maybe_new_creds {
|
||||
info!("Storing updated credentials.");
|
||||
sora_creds_manager.set_credentials(&new_creds)?;
|
||||
}
|
||||
|
||||
info!("New Sora Task ID: {:?} ", response.task_id);
|
||||
|
||||
sora_task_queue.insert(&response.task_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
pub mod commands;
|
||||
pub mod events;
|
||||
pub mod state;
|
||||
pub mod threads;
|
||||
pub mod utils;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
use crate::commands::flip_image::flip_image;
|
||||
use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command;
|
||||
use crate::commands::app_preferences::update_app_preference_command::update_app_preferences_command;
|
||||
use crate::commands::platform_info_command::platform_info_command;
|
||||
use crate::commands::sora::open_sora_login_command::open_sora_login_command;
|
||||
use crate::commands::sora::sora_image_generation_command::sora_image_generation_command;
|
||||
use crate::commands::sora::sora_image_remix_command::sora_image_remix_command;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use crate::state::main_window_size::MainWindowSize;
|
||||
use crate::state::sora::sora_credential_manager::SoraCredentialManager;
|
||||
use crate::state::sora::sora_task_queue::SoraTaskQueue;
|
||||
use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager;
|
||||
use crate::threads::discord_presence_thread::discord_presence_thread;
|
||||
use crate::threads::main_window_thread::main_window_thread::main_window_thread;
|
||||
use crate::threads::sora_session_login_thread::sora_session_login_thread;
|
||||
use crate::threads::sora_task_polling_thread::sora_task_polling_thread;
|
||||
use crate::utils::webview_unsafe::webview_unsafe_for_app;
|
||||
|
||||
use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default;
|
||||
use tauri_plugin_http;
|
||||
use tauri_plugin_dialog;
|
||||
use tauri_plugin_log::Target;
|
||||
use tauri_plugin_log::TargetKind;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// NB: Tauri wants to install the logger itself, so we can't rely on the logger crate
|
||||
// until the tauri runtime begins.
|
||||
println!("Loading config...");
|
||||
let app_data_root = AppDataRoot::create_default().expect("data directory should be created");
|
||||
let app_data_root_2 = app_data_root.clone();
|
||||
|
||||
println!("Loading app preferences...");
|
||||
let app_preferences = load_app_preferences_or_default(&app_data_root);
|
||||
|
||||
println!("Attempting to read existing artcraft credentials...");
|
||||
let storyteller_creds_manager = StorytellerCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
let storyteller_creds_manager_2 = storyteller_creds_manager.clone();
|
||||
|
||||
println!("Attempting to read existing credentials...");
|
||||
let sora_creds_manager = SoraCredentialManager::initialize_from_disk_infallible(&app_data_root);
|
||||
let sora_creds_manager_2 = sora_creds_manager.clone();
|
||||
|
||||
// Other state
|
||||
let sora_task_queue = SoraTaskQueue::new();
|
||||
let sora_task_queue_2 = sora_task_queue.clone();
|
||||
|
||||
println!("Initializing backend runtime...");
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_upload::init())
|
||||
.plugin(tauri_plugin_log::Builder::new()
|
||||
.level(log::LevelFilter::Info)
|
||||
.targets(vec![
|
||||
Target::new(TargetKind::Stdout),
|
||||
Target::new(TargetKind::LogDir {
|
||||
file_name: Some(app_data_root.log_file_name_str().to_string())
|
||||
}),
|
||||
])
|
||||
.build())
|
||||
.setup(move |app| {
|
||||
// TODO(bt): This is broken on windows
|
||||
// log_environment_details();
|
||||
|
||||
//if cfg!(debug_assertions) {
|
||||
// app.handle().plugin(
|
||||
// tauri_plugin_log::Builder::default()
|
||||
// .level(log::LevelFilter::Info)
|
||||
// .build(),
|
||||
// )?;
|
||||
//}
|
||||
let app = app.handle().clone();
|
||||
|
||||
let result = webview_unsafe_for_app(&app);
|
||||
if let Err(err) = result {
|
||||
eprintln!("Error setting webview unsafe: {:?}", err);
|
||||
}
|
||||
|
||||
let app_2 = app.clone();
|
||||
let app_3 = app.clone();
|
||||
let app_4 = app.clone();
|
||||
let app_data_root_3 = app_data_root_2.clone();
|
||||
let app_data_root_4 = app_data_root_2.clone();
|
||||
let sora_creds_manager_3 = sora_creds_manager_2.clone();
|
||||
let storyteller_creds_manager_3 = storyteller_creds_manager.clone();
|
||||
|
||||
match MainWindowSize::from_filesystem_configs(&app_data_root_3) {
|
||||
Ok(None) => {}
|
||||
Ok(Some(size)) => {
|
||||
println!("Resizing window to: {:?}", size);
|
||||
let result = size.apply_to_main_window(&app);
|
||||
if let Err(err) = result {
|
||||
eprintln!("Could not set window size: {:?}", err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Failed to read window size from disk: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(sora_session_login_thread(app_2, app_data_root_2, sora_creds_manager_2));
|
||||
tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2));
|
||||
tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2));
|
||||
tauri::async_runtime::spawn(discord_presence_thread());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.manage(app_data_root)
|
||||
.manage(app_preferences)
|
||||
.manage(sora_creds_manager)
|
||||
.manage(sora_task_queue)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
flip_image,
|
||||
get_app_preferences_command,
|
||||
update_app_preferences_command,
|
||||
open_sora_login_command,
|
||||
platform_info_command,
|
||||
sora_image_generation_command,
|
||||
sora_image_remix_command,
|
||||
])
|
||||
.run(tauri::generate_context!("tauri.conf.json"))
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
artcraft_app_lib::run();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use crate::state::app_preferences::app_preferences_serializable::AppPreferencesSerializable;
|
||||
use crate::state::app_preferences::preferred_download_directory::{PreferredDownloadDirectory, SystemDownloadDirectory};
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppPreferences {
|
||||
/// The downloads directory to use when a user downloads a file.
|
||||
pub preferred_download_directory: PreferredDownloadDirectory,
|
||||
|
||||
/// Play sounds on events.
|
||||
pub play_sounds: bool,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_success_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_failure_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_enqueue_sound: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AppPreferences {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
preferred_download_directory: PreferredDownloadDirectory::System(SystemDownloadDirectory::Downloads),
|
||||
play_sounds: true,
|
||||
// NB: These are defined in the frontend.
|
||||
generation_success_sound: Some("flower".to_string()),
|
||||
generation_failure_sound: Some("crumble".to_string()),
|
||||
generation_enqueue_sound: Some("done".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppPreferences {
|
||||
pub fn load_from_file_or_default(data_root: &AppDataRoot) -> Self {
|
||||
let filename = data_root.settings_dir().get_app_preferences_path();
|
||||
if !filename.exists() {
|
||||
return Self::default();
|
||||
}
|
||||
|
||||
match AppPreferencesSerializable::load_from_file(data_root) {
|
||||
Ok(Some(serializable)) => {
|
||||
serializable.to_preferences()
|
||||
}
|
||||
Ok(None) => {
|
||||
Self::default()
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error loading app preferences: {}", err);
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_serializable(&self) -> AppPreferencesSerializable {
|
||||
AppPreferencesSerializable::from_preferences(self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use crate::state::app_preferences::app_preferences::AppPreferences;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use memory_store::clone_cell::CloneCell;
|
||||
|
||||
pub type AppPreferencesManager = CloneCell<AppPreferences>;
|
||||
|
||||
pub fn load_app_preferences_or_default(data_root: &AppDataRoot) -> AppPreferencesManager {
|
||||
let prefs = AppPreferences::load_from_file_or_default(data_root);
|
||||
AppPreferencesManager::with_owned(prefs)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use crate::state::app_preferences::app_preferences::AppPreferences;
|
||||
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
/// Vector clock versioning string rather than semver.
|
||||
const CURRENT_VERSION: &str = "1";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppPreferencesSerializable {
|
||||
/// Versioning string.
|
||||
pub version: String,
|
||||
|
||||
/// The downloads directory to use when a user downloads a file.
|
||||
pub preferred_download_directory: Option<PreferredDownloadDirectory>,
|
||||
|
||||
/// Play sounds on events.
|
||||
pub play_sounds: Option<bool>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_success_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_failure_sound: Option<String>,
|
||||
|
||||
/// Key pointing to file; defined in the frontend code.
|
||||
pub generation_enqueue_sound: Option<String>,
|
||||
}
|
||||
|
||||
impl AppPreferencesSerializable {
|
||||
pub fn load_from_file(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
|
||||
let filename = app_data_root.settings_dir().get_app_preferences_path();
|
||||
if !filename.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let contents = std::fs::read_to_string(filename)?;
|
||||
let data: Self = serde_json::from_str(&contents)?;
|
||||
Ok(Some(data))
|
||||
}
|
||||
|
||||
pub fn from_preferences(preferences: &AppPreferences) -> Self {
|
||||
Self {
|
||||
version: CURRENT_VERSION.to_string(),
|
||||
preferred_download_directory: Some(preferences.preferred_download_directory.clone()),
|
||||
play_sounds: Some(preferences.play_sounds),
|
||||
generation_success_sound: preferences.generation_success_sound.clone(),
|
||||
generation_failure_sound: preferences.generation_failure_sound.clone(),
|
||||
generation_enqueue_sound: preferences.generation_enqueue_sound.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_preferences(&self) -> AppPreferences {
|
||||
let mut preferences = AppPreferences::default();
|
||||
|
||||
if let Some(preferred_download_directory) = &self.preferred_download_directory {
|
||||
preferences.preferred_download_directory = preferred_download_directory.clone();
|
||||
}
|
||||
|
||||
if let Some(play_sounds) = self.play_sounds {
|
||||
preferences.play_sounds = play_sounds;
|
||||
}
|
||||
|
||||
preferences.generation_success_sound = self.generation_success_sound.clone();
|
||||
preferences.generation_failure_sound = self.generation_failure_sound.clone();
|
||||
preferences.generation_enqueue_sound = self.generation_enqueue_sound.clone();
|
||||
|
||||
preferences
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod app_preferences;
|
||||
pub mod app_preferences_manager;
|
||||
pub mod app_preferences_serializable;
|
||||
pub mod preferred_download_directory;
|
||||
@@ -0,0 +1,82 @@
|
||||
use errors::AnyhowError;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
const SYSTEM_DEFAULT_SENTINEL_VALUE: &str = "system_default";
|
||||
|
||||
/// NB: The reason these are not flat is so that they serialize/deserialize nicely to/from JSON.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PreferredDownloadDirectory {
|
||||
/// The system-default downloads directory, which varies by OS.
|
||||
/// NB: Serializes as `"system_default"` in JSON.
|
||||
System(SystemDownloadDirectory),
|
||||
|
||||
/// A user-defined path
|
||||
/// NB: Serializes as `{"custom": "/path/to/dir"}` in JSON.
|
||||
Custom(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SystemDownloadDirectory {
|
||||
Downloads,
|
||||
Documents,
|
||||
}
|
||||
|
||||
impl FromStr for SystemDownloadDirectory {
|
||||
type Err = AnyhowError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match (s) {
|
||||
"downloads" => Ok(SystemDownloadDirectory::Downloads),
|
||||
"documents" => Ok(SystemDownloadDirectory::Documents),
|
||||
_ => Err(AnyhowError::msg(format!("Invalid system download directory: {}", s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PreferredDownloadDirectory {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
PreferredDownloadDirectory::System(system) => system.fmt(f),
|
||||
PreferredDownloadDirectory::Custom(path) => write!(f, "{}", path.to_string_lossy()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SystemDownloadDirectory {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SystemDownloadDirectory::Downloads => write!(f, "downloads"),
|
||||
SystemDownloadDirectory::Documents => write!(f, "documents"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::state::app_preferences::preferred_download_directory::PreferredDownloadDirectory;
|
||||
use crate::state::app_preferences::preferred_download_directory::SystemDownloadDirectory;
|
||||
use std::str::FromStr;
|
||||
|
||||
mod json {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn to_json_system_default() {
|
||||
let val = PreferredDownloadDirectory::System(SystemDownloadDirectory::Documents);
|
||||
let val = serde_json::to_string(&val).unwrap();
|
||||
assert_eq!(&val, "{\"system\":\"documents\"}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_json_custom() {
|
||||
let val = PreferredDownloadDirectory::Custom("/tmp".into());
|
||||
let val = serde_json::to_string(&val).unwrap();
|
||||
assert_eq!(&val, "{\"custom\":\"/tmp\"}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
|
||||
pub struct AppStartupTime {
|
||||
startup_time: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl AppStartupTime {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
startup_time: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_startup_time(&self) -> DateTime<Utc> {
|
||||
self.startup_time
|
||||
}
|
||||
|
||||
pub fn time_delta_since(&self) -> TimeDelta {
|
||||
let now = Utc::now();
|
||||
now.signed_duration_since(self.startup_time)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use chrono::{DateTime, Local};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppAssetsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppAssetsDir {
|
||||
const DIRECTORY_NAME: &'static str = "assets";
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl AppAssetsDir {
|
||||
/// Return the current date asset directory
|
||||
/// If it doesn't exist, create it.
|
||||
pub fn make_or_get_current_date_dir(&self) -> anyhow::Result<PathBuf> {
|
||||
let format: DateTime<Local> = Local::now();
|
||||
let date_directory_name = format.format("%Y-%d-%m").to_string();
|
||||
let full_path = self.path.join(date_directory_name);
|
||||
if !full_path.exists() {
|
||||
std::fs::create_dir(&full_path)?;
|
||||
}
|
||||
Ok(full_path)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppCredentialsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppCredentialsDir {
|
||||
const DIRECTORY_NAME: &'static str = "credentials";
|
||||
|
||||
fn new_from<P: AsRef<Path>> (dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl AppCredentialsDir {
|
||||
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
|
||||
self.path.join("sora_cookies.txt")
|
||||
}
|
||||
|
||||
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
|
||||
self.path.join("sora_bearer_token.txt")
|
||||
}
|
||||
|
||||
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
|
||||
self.path.join("sora_sentinel.txt")
|
||||
}
|
||||
|
||||
pub fn get_storyteller_avt_cookie_file_path(&self) -> PathBuf {
|
||||
self.path.join("artcraft_avt.txt")
|
||||
}
|
||||
|
||||
pub fn get_storyteller_session_cookie_file_path(&self) -> PathBuf {
|
||||
self.path.join("artcraft_session.txt")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
use crate::state::data_dir::app_assets_dir::AppAssetsDir;
|
||||
use crate::state::data_dir::app_credentials_dir::AppCredentialsDir;
|
||||
use crate::state::data_dir::app_downloads_dir::AppDownloadsDir;
|
||||
use crate::state::data_dir::app_settings_dir::AppSettingsDir;
|
||||
use crate::state::data_dir::app_weights_dir::AppWeightsDir;
|
||||
use crate::state::data_dir::temporary_dir::TemporaryDir;
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use crate::state::expanduser::expanduser;
|
||||
use crate::state::os_platform::OsPlatform;
|
||||
use anyhow::anyhow;
|
||||
use directories::UserDirs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const DEFAULT_DATA_DIR : &str = "Artcraft";
|
||||
|
||||
/// Note: Tauri appends ".log" to the end of the filename.
|
||||
const LOG_FILE_NAME : &str = "application_debug";
|
||||
|
||||
/// The path to the application data directory, which includes "asset" and "weights" data.
|
||||
#[derive(Clone)]
|
||||
pub struct AppDataRoot {
|
||||
path: PathBuf,
|
||||
assets_dir: AppAssetsDir,
|
||||
credentials_dir: AppCredentialsDir,
|
||||
downloads_dir: AppDownloadsDir,
|
||||
settings_dir: AppSettingsDir,
|
||||
weights_dir: AppWeightsDir,
|
||||
temp_dir: TemporaryDir,
|
||||
log_file_name: PathBuf,
|
||||
log_file_name_string: String,
|
||||
}
|
||||
|
||||
impl AppDataRoot {
|
||||
pub fn create_default() -> anyhow::Result<Self> {
|
||||
let directory = get_default_data_dir()?;
|
||||
Self::create_existing(directory)
|
||||
}
|
||||
|
||||
pub fn create_existing<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
|
||||
match OsPlatform::get() {
|
||||
OsPlatform::Linux | OsPlatform::MacOs => {
|
||||
if let Some(d) = dir.as_os_str().to_str() {
|
||||
dir = expanduser(d)?;
|
||||
}
|
||||
},
|
||||
OsPlatform::Windows => {}
|
||||
}
|
||||
|
||||
if !dir.is_dir() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
}
|
||||
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
|
||||
let assets_dir = AppAssetsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let credentials_dir = AppCredentialsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let downloads_dir = AppDownloadsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let settings_dir = AppSettingsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let temp_dir = TemporaryDir::get_or_create_in_root_dir(&dir)?;
|
||||
let weights_dir = AppWeightsDir::get_or_create_in_root_dir(&dir)?;
|
||||
let log_file_name = dir.join(LOG_FILE_NAME);
|
||||
let log_file_name_string = log_file_name
|
||||
.to_str()
|
||||
.ok_or(anyhow!("couldn't convert log path to str"))?
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
path: dir,
|
||||
assets_dir,
|
||||
credentials_dir,
|
||||
downloads_dir,
|
||||
settings_dir,
|
||||
weights_dir,
|
||||
temp_dir,
|
||||
log_file_name,
|
||||
log_file_name_string,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn assets_dir(&self) -> &AppAssetsDir {
|
||||
&self.assets_dir
|
||||
}
|
||||
|
||||
pub fn credentials_dir(&self) -> &AppCredentialsDir {
|
||||
&self.credentials_dir
|
||||
}
|
||||
|
||||
pub fn downloads_dir(&self) -> &AppDownloadsDir {
|
||||
&self.downloads_dir
|
||||
}
|
||||
|
||||
pub fn settings_dir(&self) -> &AppSettingsDir {
|
||||
&self.settings_dir
|
||||
}
|
||||
|
||||
pub fn weights_dir(&self) -> &AppWeightsDir {
|
||||
&self.weights_dir
|
||||
}
|
||||
|
||||
pub fn temp_dir(&self) -> &TemporaryDir {
|
||||
&self.temp_dir
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn log_file_name(&self) -> &Path {
|
||||
&self.log_file_name
|
||||
}
|
||||
|
||||
pub fn log_file_name_str(&self) -> &str {
|
||||
&self.log_file_name_string
|
||||
}
|
||||
|
||||
pub fn get_sora_cookie_file_path(&self) -> PathBuf {
|
||||
self.credentials_dir().get_sora_cookie_file_path()
|
||||
}
|
||||
|
||||
pub fn get_sora_bearer_token_file_path(&self) -> PathBuf {
|
||||
self.credentials_dir().get_sora_bearer_token_file_path()
|
||||
}
|
||||
|
||||
pub fn get_sora_sentinel_file_path(&self) -> PathBuf {
|
||||
self.credentials_dir().get_sora_sentinel_file_path()
|
||||
}
|
||||
|
||||
pub fn get_window_size_config_file(&self) -> PathBuf {
|
||||
self.path.join("window_size.json")
|
||||
}
|
||||
}
|
||||
|
||||
// eg. /home/bob/artcraft, /Users/bob/artcraft, or C:\Users\Alice\artcraft
|
||||
fn get_default_data_dir() -> anyhow::Result<PathBuf> {
|
||||
Ok(UserDirs::new()
|
||||
.ok_or_else(|| anyhow!("could not determine user home directory"))?
|
||||
.home_dir()
|
||||
.join(DEFAULT_DATA_DIR))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppDownloadsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppDownloadsDir {
|
||||
const DIRECTORY_NAME: &'static str = "downloads";
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use crate::state::app_preferences::app_preferences::AppPreferences;
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use errors::AnyhowResult;
|
||||
use serde::Serialize;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// For now, let's just keep a collection of loose json files in a settings dir.
|
||||
/// Once we know the structure and schema, we can standardize on a single json file.
|
||||
#[derive(Clone)]
|
||||
pub struct AppSettingsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppSettingsDir {
|
||||
const DIRECTORY_NAME: &'static str = "settings";
|
||||
|
||||
fn new_from<P: AsRef<Path>> (dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl AppSettingsDir {
|
||||
pub fn get_app_preferences_path(&self) -> PathBuf {
|
||||
self.path.join("app_preferences.json")
|
||||
}
|
||||
|
||||
pub fn write_app_preferences(&self, prefs: &AppPreferences) -> AnyhowResult<()> {
|
||||
let serializable = prefs.to_serializable();
|
||||
|
||||
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
|
||||
let mut buf = Vec::new();
|
||||
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
|
||||
serializable.serialize(&mut ser)?;
|
||||
|
||||
let filename = self.get_app_preferences_path();
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filename)?;
|
||||
|
||||
file.write_all(&buf)?;
|
||||
file.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppWeightsDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for AppWeightsDir {
|
||||
const DIRECTORY_NAME: &'static str = "weights";
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod app_assets_dir;
|
||||
pub mod app_credentials_dir;
|
||||
pub mod app_data_root;
|
||||
pub mod app_downloads_dir;
|
||||
pub mod app_settings_dir;
|
||||
pub mod app_weights_dir;
|
||||
pub mod temporary_dir;
|
||||
pub mod trait_data_subdir;
|
||||
@@ -0,0 +1,38 @@
|
||||
use crate::state::data_dir::trait_data_subdir::DataSubdir;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempdir::TempDir;
|
||||
use tempfile::{Builder, NamedTempFile};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TemporaryDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataSubdir for TemporaryDir {
|
||||
const DIRECTORY_NAME: &'static str = "temp";
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self {
|
||||
Self {
|
||||
path: dir.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl TemporaryDir {
|
||||
pub fn with_prefix(&self, prefix: &str) -> anyhow::Result<TempDir> {
|
||||
let tempdir = TempDir::new_in(&self.path, prefix)?;
|
||||
Ok(tempdir)
|
||||
}
|
||||
|
||||
pub fn new_named_temp_file(&self) -> anyhow::Result<NamedTempFile> {
|
||||
let tempfile = Builder::new()
|
||||
.prefix("temp_")
|
||||
.suffix(".bin")
|
||||
.tempfile_in(&self.path)?;
|
||||
Ok(tempfile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use std::path::Path;
|
||||
|
||||
pub trait DataSubdir : Sized {
|
||||
const DIRECTORY_NAME: &'static str;
|
||||
|
||||
fn new_from<P: AsRef<Path>>(dir: P) -> Self;
|
||||
|
||||
fn path(&self) -> &Path;
|
||||
|
||||
fn get_or_create_in_root_dir<P: AsRef<Path>>(root_dir: P) -> anyhow::Result<Self> {
|
||||
Self::get_or_create_dir(root_dir.as_ref().join(Self::DIRECTORY_NAME))
|
||||
}
|
||||
|
||||
fn get_or_create_dir<P: AsRef<Path>>(dir: P) -> anyhow::Result<Self> {
|
||||
let mut dir = dir.as_ref().to_path_buf();
|
||||
|
||||
match dir.canonicalize() {
|
||||
Ok(d) => dir = d,
|
||||
Err(err) => {
|
||||
println!("Error canonicalizing {:?}: {}", dir, err);
|
||||
}
|
||||
}
|
||||
|
||||
if !dir.exists() {
|
||||
println!("Creating directory {:?}", dir);
|
||||
std::fs::create_dir(&dir)?;
|
||||
}
|
||||
|
||||
Ok(Self::new_from(dir))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use errors::AnyhowResult;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use tauri::{AppHandle, Manager, PhysicalSize, Window};
|
||||
|
||||
const MAIN_WINDOW_NAME: &str = "main";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MainWindowSize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl MainWindowSize {
|
||||
pub fn new(width: u32, height: u32) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
|
||||
pub fn from_main_window(app: &AppHandle) -> AnyhowResult<Self> {
|
||||
let windows = app.windows();
|
||||
windows.get(MAIN_WINDOW_NAME)
|
||||
.map(|window| Self::from_window(window))
|
||||
.unwrap_or_else(|| Err(anyhow::anyhow!("Main window not found")))
|
||||
}
|
||||
|
||||
pub fn from_window(window: &Window) -> AnyhowResult<Self> {
|
||||
let size = window.inner_size()?;
|
||||
Ok(Self {
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_filesystem_configs(app_data_root: &AppDataRoot) -> AnyhowResult<Option<Self>> {
|
||||
let filename = app_data_root.get_window_size_config_file();
|
||||
if !filename.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let contents = std::fs::read_to_string(filename)?;
|
||||
let size: MainWindowSize = serde_json::from_str(&contents)?;
|
||||
Ok(Some(size))
|
||||
}
|
||||
|
||||
pub fn apply_to_main_window(&self, app: &AppHandle) -> AnyhowResult<()> {
|
||||
let windows = app.windows();
|
||||
let window = windows.get(MAIN_WINDOW_NAME)
|
||||
.ok_or_else(|| anyhow::anyhow!("Main window not found"))?;
|
||||
window.set_size(PhysicalSize::new(self.width, self.height))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn persist_to_filesystem(&self, app_data_root: &AppDataRoot) -> AnyhowResult<()> {
|
||||
let filename = app_data_root.get_window_size_config_file();
|
||||
let json = serde_json::to_string(self)?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filename)?;
|
||||
file.write_all(json.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_physical_size(&self) -> PhysicalSize<u32> {
|
||||
PhysicalSize::new(self.width, self.height)
|
||||
}
|
||||
|
||||
pub fn matches_physical_size(&self, size: &PhysicalSize<u32>) -> bool {
|
||||
self.width == size.width && self.height == size.height
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod app_preferences;
|
||||
pub mod app_startup_time;
|
||||
pub mod data_dir;
|
||||
pub mod expanduser;
|
||||
pub mod main_window_size;
|
||||
pub mod os_platform;
|
||||
pub mod sora;
|
||||
pub mod storyteller;
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
#[derive(Copy,Clone,Eq,PartialEq,Debug)]
|
||||
pub enum OsPlatform {
|
||||
Linux,
|
||||
MacOs,
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl OsPlatform {
|
||||
pub fn maybe_get() -> Option<Self> {
|
||||
// Totally scientific.
|
||||
// https://doc.rust-lang.org/std/env/consts/constant.OS.html
|
||||
match std::env::consts::OS {
|
||||
"linux" => Some(Self::Linux),
|
||||
"windows" => Some(Self::Windows),
|
||||
"macos" | "apple" => Some(Self::MacOs),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get() -> Self {
|
||||
Self::maybe_get().unwrap_or(Self::Linux)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod read_sora_credentials_from_disk;
|
||||
pub mod read_sora_credentials_from_disk_with_upgrade;
|
||||
pub mod sora_credential_holder;
|
||||
pub mod sora_credential_manager;
|
||||
pub mod sora_task_queue;
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use openai_sora_client::creds::sora_cookies::SoraCookies;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
pub fn read_sora_credentials_from_disk(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentialSet> {
|
||||
let cookie_file = app_data_root.get_sora_cookie_file_path();
|
||||
let bearer_file = app_data_root.get_sora_bearer_token_file_path();
|
||||
let sentinel_file = app_data_root.get_sora_sentinel_file_path();
|
||||
|
||||
if !cookie_file.exists() {
|
||||
return Err(anyhow!("Cookie file does not exist: {:?}", cookie_file));
|
||||
}
|
||||
|
||||
let value = read_to_string(cookie_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let cookie = SoraCookies::new(value);
|
||||
|
||||
let mut bearer = None;
|
||||
let mut sentinel = None;
|
||||
|
||||
if bearer_file.exists() {
|
||||
let value = read_to_string(&bearer_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
bearer = Some(SoraJwtBearerToken::new(value)?);
|
||||
}
|
||||
|
||||
if sentinel_file.exists() {
|
||||
let value = read_to_string(&sentinel_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
sentinel = Some(SoraSentinel::new(value));
|
||||
}
|
||||
|
||||
Ok(SoraCredentialSet::initialize(
|
||||
cookie,
|
||||
bearer,
|
||||
sentinel,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use crate::state::data_dir::app_data_root::AppDataRoot;
|
||||
use anyhow::anyhow;
|
||||
use errors::AnyhowResult;
|
||||
use log::{info, warn};
|
||||
use openai_sora_client::creds::sora_cookies::SoraCookies;
|
||||
use openai_sora_client::creds::sora_credential_set::SoraCredentialSet;
|
||||
use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
|
||||
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
|
||||
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
|
||||
use std::fs::{read_to_string, OpenOptions};
|
||||
use std::io::Write;
|
||||
|
||||
pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentialSet> {
|
||||
let cookie_file = app_data_root.get_sora_cookie_file_path();
|
||||
let bearer_file = app_data_root.get_sora_bearer_token_file_path();
|
||||
let sentinel_file = app_data_root.get_sora_sentinel_file_path();
|
||||
|
||||
if !cookie_file.exists() {
|
||||
return Err(anyhow!("Cookie file does not exist: {:?}", cookie_file));
|
||||
}
|
||||
|
||||
let value = read_to_string(cookie_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
let cookie = SoraCookies::new(value);
|
||||
|
||||
let mut bearer = None;
|
||||
let mut sentinel = None;
|
||||
|
||||
if bearer_file.exists() {
|
||||
let value = read_to_string(&bearer_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
bearer = Some(SoraJwtBearerToken::new(value)?);
|
||||
}
|
||||
|
||||
if sentinel_file.exists() {
|
||||
let value = read_to_string(&sentinel_file)?
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
sentinel = Some(SoraSentinel::new(value));
|
||||
}
|
||||
|
||||
let mut credentials = SoraCredentialSet::initialize(
|
||||
cookie,
|
||||
bearer,
|
||||
sentinel,
|
||||
);
|
||||
|
||||
let response = maybe_upgrade_or_renew_session(&mut credentials).await;
|
||||
|
||||
if let Err(err) = response {
|
||||
// NB: Make this portion infallible. Don't die on setup. We can recover downstream.
|
||||
warn!("Failed to upgrade or renew session: {:?}", err);
|
||||
}
|
||||
|
||||
if !bearer_file.exists() {
|
||||
if let Some(bearer) = &credentials.jwt_bearer_token {
|
||||
info!("Persisting bearer token value...");
|
||||
let value = bearer.token_str();
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&bearer_file)?;
|
||||
|
||||
file.write_all(value.as_bytes())?;
|
||||
file.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
if !sentinel_file.exists() {
|
||||
if let Some(sentinel) = &credentials.sora_sentinel {
|
||||
info!("Persisting sentinel value...");
|
||||
let value = sentinel.get_sentinel();
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&sentinel_file)?;
|
||||
|
||||
file.write_all(value.as_bytes())?;
|
||||
file.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||