From 2c3c86e07d4c5da4e7f7dd43d5121dfccc255ed5 Mon Sep 17 00:00:00 2001 From: RTSDA Date: Sat, 16 Aug 2025 18:57:27 -0400 Subject: [PATCH] Add LICENSE, improve README, and update project documentation - Add MIT LICENSE file - Make README more generic and comprehensive - Add installation and troubleshooting sections - Include systemd service integration docs - Update with build and deployment instructions --- .DS_Store | Bin 0 -> 8196 bytes DEPLOY.md | 104 +++ LICENSE | 21 + README.md | 130 ++++ deploy.sh | 87 +++ livestream-archiver-clean.tar.gz | Bin 0 -> 12216 bytes livestream-archiver.service | 16 + package.sh | 94 +++ package/Cargo.lock | 748 ++++++++++++++++++++ package/Cargo.toml | 14 + package/DEPLOY.md | 104 +++ package/README.md | 59 ++ package/livestream-archiver-clean.tar.gz | Bin 0 -> 12931 bytes package/livestream-archiver.service | 16 + package/src/main.rs | 134 ++++ package/src/services/livestream_archiver.rs | 313 ++++++++ package/src/services/mod.rs | 1 + src/.DS_Store | Bin 0 -> 6148 bytes src/main.rs | 26 +- src/services/livestream_archiver.rs | 94 ++- 20 files changed, 1949 insertions(+), 12 deletions(-) create mode 100644 .DS_Store create mode 100644 DEPLOY.md create mode 100644 LICENSE create mode 100644 README.md create mode 100755 deploy.sh create mode 100644 livestream-archiver-clean.tar.gz create mode 100644 livestream-archiver.service create mode 100755 package.sh create mode 100644 package/Cargo.lock create mode 100644 package/Cargo.toml create mode 100644 package/DEPLOY.md create mode 100644 package/README.md create mode 100644 package/livestream-archiver-clean.tar.gz create mode 100644 package/livestream-archiver.service create mode 100644 package/src/main.rs create mode 100644 package/src/services/livestream_archiver.rs create mode 100644 package/src/services/mod.rs create mode 100644 src/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..00cf1ef78dcc5cb24ee2820a0777e41657c61e0b GIT binary patch literal 8196 zcmeHML2nyH6nL@lR6t3sBuVm!NG)jVau$yx=gSFf~%XA=RAY~wBAY~wB zAZ6f1VF1r;UX)YL{ZN~>DFZ131IYm2A2h67mZa>4QfwXAh${ffAsiM3`zr6C)TAs) z*$pLXu#ganGK3O&#ULRZ$2DnJlCm4h5Dr9MK8W;8N6E%z>0bnYJkdDFgis z@VomET!AK75XA29j|$zWuEi>qhRqr{rab)omiFcEj~C;5^I!GuZsgm*&*Ci|fhEwv zL*EP|u(35@Z`o#s^uvwHSg$-41$zK-On{gc*a^%J2}}+ua2r!Dk zhcJPHD`Gl8gb%xMM7EkhXlQFe2T?u*m=nd!TJVO*&wyR2^KisKORjZr-azO%`w!yt zi@d*zkbic0VmzWdimP0)OuuO_3 zTX!wTWcy{-c1&MjYoS8PDY?2@I6R!2o1f7Zt}PtRXoqiJo1M|-uP+=O<&>%7)mzp2 zp51cT#~3*lA`d$l3x7XvKf#{N#xgoG8}dspn?F>wmrrhF^rf-!bLzz8`HB3*)YR04 zmkX1xT%4|}=Nr1Q*YtRtOU!0o*>2Oidd|memks8udbERCt_SqPpk_Tl)no!trRMv` zLu&^v+T@Q1#H4R~lscPD&oWnbc6=JFNBSGwo8d9rUS+-&Sj^ej65a#aT6T6AdgKwT zxLoDmp`MSj53_#H)*IAbamp+RSZhP~Jl$#1^1-@=S@6h(%bKejmeVZ0^>;{Ou2_0K z?5R-f@v7u|Jt;}?C%P^AxiyD5v{+gVHO109(nNvF_^8%k3qF7@d;wp>ckl?Fz+dnW z$&xX0fm|Y&$qczcZjusNBvrCWwunydhxklyiC>lTNkM$<$R&<#%dqhHSQFipuRne5 zw{N}`o3g~U*zuO!uZ?ZUu+9BST&LzaHglRd!lOGYw<&ztA&9?X8U7+hU$K~NH16?_ zoSVtHqS2q3{xmO-l-L~698R-|W-|Wc_vi-ym<%vlz7?Oisb#!G+#EA-B^zd3;P?OA zr+@z+I9}3(Qw9c!0WwsnRmzxg?<`6t&v$tZ>o!(ixLr4t0)h=U;p5^uj`I0G46(0C eT}jGrC{csufBF!Rp8wHPQ!Z_K{) +cd livestream-archiver + +# Build release binary +cargo build --release + +# Binary will be at target/release/livestream-archiver +``` + +### Quick Deploy + +```bash +# Use the included deploy script +./deploy.sh +``` + +## Configuration + +Set the `PC_SYNC_TARGET` environment variable to configure where files are synced: + +```bash +export PC_SYNC_TARGET="user@192.168.1.100:/path/to/destination/" +``` + +If not set, defaults to: `user@192.168.1.100:/path/to/destination/` + +## Usage + +### Running Manually + +```bash +# Run directly +cargo run + +# Or run the compiled binary +./target/release/livestream-archiver +``` + +### Running as a Service + +```bash +# Copy the service file +sudo cp livestream-archiver.service /etc/systemd/system/ + +# Reload systemd and enable the service +sudo systemctl daemon-reload +sudo systemctl enable livestream-archiver +sudo systemctl start livestream-archiver + +# Check service status +sudo systemctl status livestream-archiver +``` + +The application will: +1. Check for existing unprocessed files +2. Start monitoring for new files +3. For each detected file: + - Wait for it to be ready (stable size/modification time) + - Sync to PC using rsync + - Convert to AV1 format + - Create NFO metadata file + - Delete original file + +## Directory Structure + +Output files are organized as: +``` +/ +├── 2024/ +│ ├── 01-January/ +│ │ ├── Livestream - January 01 2024.mp4 +│ │ ├── Livestream - January 01 2024.nfo +│ │ └── ... +│ └── ... +└── ... +``` + +## Environment Variables + +- `PC_SYNC_TARGET`: Remote sync destination (e.g., `user@server:/path/to/destination/`) +- `INPUT_DIR`: Directory to monitor for new files (default: `/home/user/Sync/Livestreams`) +- `OUTPUT_DIR`: Local output directory for processed files (default: `/media/archive/jellyfin/livestreams`) + +## Troubleshooting + +### Service not starting +- Check logs: `journalctl -u livestream-archiver -f` +- Verify paths exist and have proper permissions +- Ensure ffmpeg has QSV support: `ffmpeg -hwaccels | grep qsv` + +### Sync failures +- Verify SSH key authentication to remote server +- Check network connectivity +- Review cached sync files in `~/.cache/livestream-archiver/` + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..a99d4da --- /dev/null +++ b/deploy.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Deployment script for Livestream Archiver +# Usage: ./deploy.sh + +SERVER="rockvilleav@remote.rockvilletollandsda.church" +PORT="8443" +REMOTE_PATH="/home/rockvilleav/livestream-archiver" +SERVICE_NAME="livestream-archiver" + +echo "🚀 Starting deployment to $SERVER:$PORT" + +# Build release binary +echo "📦 Building release binary..." +cargo build --release + +if [ $? -ne 0 ]; then + echo "❌ Build failed!" + exit 1 +fi + +echo "✅ Build successful!" + +# Create deployment directory +echo "📁 Creating deployment directory..." +mkdir -p deploy + +# Copy necessary files to deploy directory +echo "📋 Copying files..." +cp target/release/livestream_archiver deploy/ +cp livestream-archiver.service deploy/ +cp README.md deploy/ + +# Create deployment package +echo "📦 Creating deployment package..." +tar -czf livestream-archiver-deploy.tar.gz -C deploy . + +# Copy to server +echo "🔄 Copying to server..." +scp -P $PORT livestream-archiver-deploy.tar.gz $SERVER:/tmp/ + +# Deploy on server +echo "🚀 Deploying on server..." +ssh -p $PORT $SERVER << 'EOF' + # Stop existing service if running + sudo systemctl stop livestream-archiver 2>/dev/null || true + + # Create application directory + sudo mkdir -p /home/rockvilleav/livestream-archiver + + # Extract deployment package + cd /tmp + tar -xzf livestream-archiver-deploy.tar.gz -C /home/rockvilleav/livestream-archiver/ + + # Set permissions + sudo chown -R rockvilleav:rockvilleav /home/rockvilleav/livestream-archiver + sudo chmod +x /home/rockvilleav/livestream-archiver/livestream_archiver + + # Install systemd service + sudo cp /home/rockvilleav/livestream-archiver/livestream-archiver.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable livestream-archiver + + # Create required directories + sudo mkdir -p /home/rockvilleav/Sync/Livestreams + sudo mkdir -p /media/archive/jellyfin/livestreams + sudo chown -R rockvilleav:rockvilleav /home/rockvilleav/Sync/Livestreams + + # Start service + sudo systemctl start livestream-archiver + + # Check status + echo "📊 Service status:" + sudo systemctl status livestream-archiver --no-pager + + # Clean up + rm -f /tmp/livestream-archiver-deploy.tar.gz +EOF + +echo "✅ Deployment complete!" +echo "📊 To check logs: ssh -p $PORT $SERVER 'sudo journalctl -u livestream-archiver -f'" +echo "🔄 To restart: ssh -p $PORT $SERVER 'sudo systemctl restart livestream-archiver'" + +# Clean up local files +rm -rf deploy livestream-archiver-deploy.tar.gz + +echo "🎉 All done!" \ No newline at end of file diff --git a/livestream-archiver-clean.tar.gz b/livestream-archiver-clean.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fe41348a238bb4a65bdb740c26343aa894b94e1b GIT binary patch literal 12216 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 G$^ZZ{GbLyM literal 0 HcmV?d00001 diff --git a/livestream-archiver.service b/livestream-archiver.service new file mode 100644 index 0000000..9552b0a --- /dev/null +++ b/livestream-archiver.service @@ -0,0 +1,16 @@ +[Unit] +Description=Livestream Archiver Service +After=network.target + +[Service] +Type=simple +User=rockvilleav +Group=rockvilleav +WorkingDirectory=/home/rockvilleav/livestream-archiver +ExecStart=/home/rockvilleav/livestream-archiver/target/release/livestream_archiver +Environment=PC_SYNC_TARGET=benjaminslingo@macbook-pro.slingoapps.dev:~/rtsda/livestreams/ +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/package.sh b/package.sh new file mode 100755 index 0000000..b81ba6f --- /dev/null +++ b/package.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# Package source code for deployment to ARM server +echo "📦 Creating source package for ARM deployment..." + +# Create package directory +mkdir -p package + +# Copy source files +echo "📋 Copying source files..." +cp -r src/ package/ +cp Cargo.toml package/ +cp Cargo.lock package/ +cp livestream-archiver.service package/ +cp README.md package/ +cp DEPLOY.md package/ + +# Create deployment script for the server +cat > package/build-and-deploy.sh << 'EOF' +#!/bin/bash + +echo "🔧 Building on ARM server..." + +# Install Rust if not present +if ! command -v cargo &> /dev/null; then + echo "📥 Installing Rust..." + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source ~/.cargo/env +fi + +# Build release +echo "📦 Building release binary..." +cargo build --release + +if [ $? -ne 0 ]; then + echo "❌ Build failed!" + exit 1 +fi + +# Stop existing service if running +echo "🛑 Stopping existing service..." +sudo systemctl stop livestream-archiver 2>/dev/null || true + +# Create directories +echo "📁 Creating directories..." +sudo mkdir -p /home/rockvilleav/livestream-archiver +sudo mkdir -p /home/rockvilleav/Sync/Livestreams +sudo mkdir -p /media/archive/jellyfin/livestreams + +# Install binary +echo "📦 Installing binary..." +sudo cp target/release/livestream_archiver /home/rockvilleav/livestream-archiver/ +sudo chmod +x /home/rockvilleav/livestream-archiver/livestream_archiver + +# Install service +echo "⚙️ Installing systemd service..." +sudo cp livestream-archiver.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable livestream-archiver + +# Set permissions +echo "🔐 Setting permissions..." +sudo chown -R rockvilleav:rockvilleav /home/rockvilleav/livestream-archiver +sudo chown -R rockvilleav:rockvilleav /home/rockvilleav/Sync/Livestreams + +# Start service +echo "🚀 Starting service..." +sudo systemctl start livestream-archiver + +# Show status +echo "📊 Service status:" +sudo systemctl status livestream-archiver --no-pager + +echo "✅ Deployment complete!" +echo "📊 To check logs: sudo journalctl -u livestream-archiver -f" +echo "🔄 To restart: sudo systemctl restart livestream-archiver" +EOF + +chmod +x package/build-and-deploy.sh + +# Create the tar.gz package (without macOS extended attributes) +echo "📦 Creating tar.gz package..." +COPYFILE_DISABLE=1 tar --no-xattrs -czf livestream-archiver-source.tar.gz -C package . + +# Clean up +rm -rf package + +echo "✅ Package created: livestream-archiver-source.tar.gz" +echo "" +echo "📤 To deploy:" +echo "1. scp -P 8443 livestream-archiver-source.tar.gz rockvilleav@remote.rockvilletollandsda.church:/tmp/" +echo "2. ssh -p 8443 rockvilleav@remote.rockvilletollandsda.church" +echo "3. cd /tmp && tar -xzf livestream-archiver-source.tar.gz" +echo "4. ./build-and-deploy.sh" \ No newline at end of file diff --git a/package/Cargo.lock b/package/Cargo.lock new file mode 100644 index 0000000..fde4950 --- /dev/null +++ b/package/Cargo.lock @@ -0,0 +1,748 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" + +[[package]] +name = "cc" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "livestream_archiver" +version = "0.1.0" +dependencies = [ + "anyhow", + "chrono", + "notify", + "tokio", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.6.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio 1.0.3", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/package/Cargo.toml b/package/Cargo.toml new file mode 100644 index 0000000..9e22573 --- /dev/null +++ b/package/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "livestream_archiver" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { version = "1.36", features = ["full"] } +anyhow = "1.0" +notify = "6.1" +chrono = "0.4" + + +# We don't need regex or other conversion-related deps +# since we're just copying and renaming files diff --git a/package/DEPLOY.md b/package/DEPLOY.md new file mode 100644 index 0000000..f9f3bcd --- /dev/null +++ b/package/DEPLOY.md @@ -0,0 +1,104 @@ +# Deployment Instructions + +## Prerequisites + +Before deploying, ensure: + +1. **SSH Key Setup**: You have passwordless SSH access to the server: + ```bash + ssh -p 8443 rockvilleav@remote.rockvilletollandsda.church + ``` + +2. **Mac SSH Setup**: The server can SSH to your Mac on port 8443: + ```bash + # Test from the server: + ssh -p 8443 benjaminslingo@macbook-pro.slingoapps.dev + ``` + +3. **Directory on Mac**: Create the destination directory: + ```bash + mkdir -p ~/rtsda/livestreams + ``` + +## Deployment + +Simply run the deployment script: + +```bash +./deploy.sh +``` + +This will: +- Build the release binary +- Package all necessary files +- Copy to the server +- Install as a systemd service +- Start the service +- Show service status + +## Manual Deployment (if script fails) + +1. **Build locally**: + ```bash + cargo build --release + ``` + +2. **Copy files to server**: + ```bash + scp -P 8443 target/release/livestream_archiver rockvilleav@remote.rockvilletollandsda.church:/tmp/ + scp -P 8443 livestream-archiver.service rockvilleav@remote.rockvilletollandsda.church:/tmp/ + ``` + +3. **Install on server**: + ```bash + ssh -p 8443 rockvilleav@remote.rockvilletollandsda.church + + # Create directories + sudo mkdir -p /home/rockvilleav/livestream-archiver + sudo mkdir -p /home/rockvilleav/Sync/Livestreams + sudo mkdir -p /media/archive/jellyfin/livestreams + + # Move binary + sudo mv /tmp/livestream_archiver /home/rockvilleav/livestream-archiver/ + sudo chmod +x /home/rockvilleav/livestream-archiver/livestream_archiver + + # Install service + sudo mv /tmp/livestream-archiver.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable livestream-archiver + sudo systemctl start livestream-archiver + + # Set permissions + sudo chown -R rockvilleav:rockvilleav /home/rockvilleav/livestream-archiver + sudo chown -R rockvilleav:rockvilleav /home/rockvilleav/Sync/Livestreams + ``` + +## Management Commands + +```bash +# Check status +sudo systemctl status livestream-archiver + +# View logs +sudo journalctl -u livestream-archiver -f + +# Restart service +sudo systemctl restart livestream-archiver + +# Stop service +sudo systemctl stop livestream-archiver +``` + +## Testing + +1. **Place a test file** in `/home/rockvilleav/Sync/Livestreams/` (name it like `2024-07-21_10-30-00.mp4`) +2. **Check logs** to see if it detects and processes the file +3. **Verify sync** to your Mac at `~/rtsda/livestreams/` +4. **Check Jellyfin** output at `/media/archive/jellyfin/livestreams/` + +## Troubleshooting + +- **SSH connection issues**: Verify port 8443 is open and SSH keys are set up +- **Permission errors**: Ensure directories have correct ownership (`rockvilleav:rockvilleav`) +- **rsync failures**: Check network connectivity and SSH key authentication +- **Service won't start**: Check logs with `journalctl -u livestream-archiver` \ No newline at end of file diff --git a/package/README.md b/package/README.md new file mode 100644 index 0000000..d7d42eb --- /dev/null +++ b/package/README.md @@ -0,0 +1,59 @@ +# Livestream Archiver + +A Rust application that monitors a directory for livestream recordings, processes them, and syncs them to a PC. + +## Features + +- **File Detection**: Monitors `/home/rockvilleav/Sync/Livestreams` for new MP4 files +- **Readiness Check**: Waits for files to be completely written before processing +- **PC Sync**: Uses rsync to send files to your PC immediately after detection +- **Caching & Retry**: Caches failed syncs and retries them on subsequent runs +- **Processing**: Converts files to AV1 using QSV hardware acceleration +- **Organization**: Creates date-based directory structure in Jellyfin format +- **Cleanup**: Deletes original files after successful processing and sync + +## Configuration + +Set the `PC_SYNC_TARGET` environment variable to configure where files are synced: + +```bash +export PC_SYNC_TARGET="user@192.168.1.100:/path/to/destination/" +``` + +If not set, defaults to: `user@192.168.1.100:/path/to/destination/` + +## Usage + +```bash +cargo run +``` + +The application will: +1. Check for existing unprocessed files +2. Start monitoring for new files +3. For each detected file: + - Wait for it to be ready (stable size/modification time) + - Sync to PC using rsync + - Convert to AV1 format + - Create NFO metadata file + - Delete original file + +## Dependencies + +- `rsync` must be installed and accessible in PATH +- `ffmpeg` with QSV support for hardware acceleration +- SSH key authentication should be set up for passwordless rsync + +## Directory Structure + +Output files are organized as: +``` +/media/archive/jellyfin/livestreams/ +├── 2024/ +│ ├── 01-January/ +│ │ ├── Divine Worship Service - RTSDA | January 01 2024.mp4 +│ │ ├── Divine Worship Service - RTSDA | January 01 2024.nfo +│ │ └── ... +│ └── ... +└── ... +``` \ No newline at end of file diff --git a/package/livestream-archiver-clean.tar.gz b/package/livestream-archiver-clean.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..af62bd7bbc120ba862ca35f6207383f3fadf8b00 GIT binary patch literal 12931 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 literal 0 HcmV?d00001 diff --git a/package/livestream-archiver.service b/package/livestream-archiver.service new file mode 100644 index 0000000..9552b0a --- /dev/null +++ b/package/livestream-archiver.service @@ -0,0 +1,16 @@ +[Unit] +Description=Livestream Archiver Service +After=network.target + +[Service] +Type=simple +User=rockvilleav +Group=rockvilleav +WorkingDirectory=/home/rockvilleav/livestream-archiver +ExecStart=/home/rockvilleav/livestream-archiver/target/release/livestream_archiver +Environment=PC_SYNC_TARGET=benjaminslingo@macbook-pro.slingoapps.dev:~/rtsda/livestreams/ +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/package/src/main.rs b/package/src/main.rs new file mode 100644 index 0000000..c299f77 --- /dev/null +++ b/package/src/main.rs @@ -0,0 +1,134 @@ +use std::path::PathBuf; +use anyhow::Result; +use notify::{Watcher, RecursiveMode, Event, EventKind}; +use tokio::sync::mpsc; +use std::collections::HashSet; +use std::sync::{Arc, Mutex}; + +mod services; +use services::livestream_archiver::LivestreamArchiver; + +#[tokio::main] +async fn main() -> Result<()> { + let watch_path = PathBuf::from("/home/rockvilleav/Sync/Livestreams"); + let output_path = PathBuf::from("/media/archive/jellyfin/livestreams"); + + // Ensure directories exist + if !watch_path.exists() { + std::fs::create_dir_all(&watch_path)?; + } + if !output_path.exists() { + std::fs::create_dir_all(&output_path)?; + } + + println!("Starting livestream archiver service..."); + println!("Watching directory: {}", watch_path.display()); + println!("Output directory: {}", output_path.display()); + + // Configure PC sync target (replace with your actual PC address and path) + let pc_sync_target = std::env::var("PC_SYNC_TARGET") + .unwrap_or_else(|_| "benjaminslingo@macbook-pro.slingoapps.dev:~/rtsda/livestreams/".to_string()); + + let archiver = Arc::new(Mutex::new( + LivestreamArchiver::with_pc_sync(output_path.clone(), pc_sync_target) + )); + let processed_files = Arc::new(Mutex::new(HashSet::new())); + + // Process existing files first + println!("Checking for existing files..."); + if let Ok(entries) = std::fs::read_dir(&watch_path) { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + // Only process .mp4 files + if path.extension().and_then(|ext| ext.to_str()) == Some("mp4") { + // Extract date from filename to check if output exists + if let Some(filename) = path.file_name().and_then(|f| f.to_str()) { + let archiver_guard = archiver.lock().unwrap(); + if let Ok(date) = archiver_guard.extract_date_from_filename(filename).await { + // Check if either Divine Worship or Afternoon Program exists for this date + let year_dir = archiver_guard.get_output_path().join(date.format("%Y").to_string()); + let month_dir = year_dir.join(format!("{}-{}", + date.format("%m"), + date.format("%B") + )); + + let divine_worship_file = month_dir.join(format!( + "Divine Worship Service - RTSDA | {}.mp4", + date.format("%B %d %Y") + )); + let afternoon_program_file = month_dir.join(format!( + "Afternoon Program - RTSDA | {}.mp4", + date.format("%B %d %Y") + )); + + if !divine_worship_file.exists() && !afternoon_program_file.exists() { + println!("Found unprocessed file: {}", path.display()); + drop(archiver_guard); // Release lock before async operation + let mut archiver_mut = archiver.lock().unwrap(); + if let Err(e) = archiver_mut.process_file(path).await { + eprintln!("Error processing existing file: {}", e); + } + } else { + println!("Skipping already processed file: {}", path.display()); + } + } + } + } + } + } + } + + // Set up file watcher for new files + let (tx, mut rx) = mpsc::channel(100); + + let mut watcher = notify::recommended_watcher(move |res: Result| { + let tx = tx.clone(); + match res { + Ok(event) => { + println!("Received event: {:?}", event); + if let Err(e) = tx.blocking_send(event) { + eprintln!("Error sending event: {}", e); + } + } + Err(e) => eprintln!("Watch error: {}", e), + } + })?; + + watcher.watch(&watch_path, RecursiveMode::NonRecursive)?; + + while let Some(event) = rx.recv().await { + println!("Processing event: {:?}", event); + + match event.kind { + EventKind::Create(_) | EventKind::Modify(_) => { + for path in event.paths { + if let Ok(canonical_path) = std::fs::canonicalize(&path) { + let path_str = canonical_path.to_string_lossy().to_string(); + let processed = processed_files.lock().unwrap(); + + if !processed.contains(&path_str) { + println!("Processing file: {}", path_str); + drop(processed); // Release processed files lock + let mut archiver_mut = archiver.lock().unwrap(); + if let Err(e) = archiver_mut.process_file(path).await { + eprintln!("Error processing file: {}", e); + } else { + let mut processed = processed_files.lock().unwrap(); + processed.insert(path_str); + if processed.len() > 1000 { + processed.clear(); + } + } + } else { + println!("Skipping already processed file: {}", path_str); + } + } + } + }, + _ => println!("Ignoring event: {:?}", event), + } + } + + Ok(()) +} diff --git a/package/src/services/livestream_archiver.rs b/package/src/services/livestream_archiver.rs new file mode 100644 index 0000000..6459fba --- /dev/null +++ b/package/src/services/livestream_archiver.rs @@ -0,0 +1,313 @@ +use std::path::PathBuf; +use anyhow::{Result, anyhow}; +use chrono::NaiveDateTime; +use tokio::process::Command; +use tokio::time::Duration; +use std::collections::VecDeque; + +pub struct LivestreamArchiver { + output_path: PathBuf, + pc_sync_target: Option, + sync_cache: VecDeque, +} + +impl LivestreamArchiver { + pub fn new(output_path: PathBuf) -> Self { + LivestreamArchiver { + output_path, + pc_sync_target: None, + sync_cache: VecDeque::new(), + } + } + + pub fn with_pc_sync(output_path: PathBuf, pc_sync_target: String) -> Self { + LivestreamArchiver { + output_path, + pc_sync_target: Some(pc_sync_target), + sync_cache: VecDeque::new(), + } + } + + pub fn get_output_path(&self) -> &PathBuf { + &self.output_path + } + + async fn sync_to_pc(&mut self, file_path: &PathBuf) -> Result<()> { + if let Some(target) = &self.pc_sync_target { + println!("Syncing {} to PC at {}", file_path.display(), target); + + let status = Command::new("rsync") + .arg("-avz") + .arg("--progress") + .arg("-e") + .arg("ssh -p 8443") + .arg(file_path) + .arg(target) + .status() + .await?; + + if status.success() { + println!("Successfully synced {} to PC", file_path.display()); + Ok(()) + } else { + println!("Failed to sync {} to PC, adding to cache", file_path.display()); + self.sync_cache.push_back(file_path.clone()); + Err(anyhow!("Rsync failed")) + } + } else { + println!("No PC sync target configured, skipping sync"); + Ok(()) + } + } + + async fn retry_cached_syncs(&mut self) -> Result<()> { + if let Some(target) = &self.pc_sync_target { + let mut successful_syncs = Vec::new(); + + for (index, file_path) in self.sync_cache.iter().enumerate() { + println!("Retrying sync for cached file: {}", file_path.display()); + + let status = Command::new("rsync") + .arg("-avz") + .arg("--progress") + .arg("-e") + .arg("ssh -p 8443") + .arg(file_path) + .arg(target) + .status() + .await?; + + if status.success() { + println!("Successfully synced cached file: {}", file_path.display()); + successful_syncs.push(index); + } else { + println!("Still failed to sync: {}", file_path.display()); + } + } + + // Remove successfully synced files from cache (in reverse order to maintain indices) + for &index in successful_syncs.iter().rev() { + self.sync_cache.remove(index); + } + } + Ok(()) + } + + async fn wait_for_file_ready(&self, path: &PathBuf) -> Result<()> { + println!("Waiting for file to be ready: {}", path.display()); + + // Initial delay - let OBS get started + tokio::time::sleep(Duration::from_secs(10)).await; + + let mut last_size = 0; + let mut stable_count = 0; + let mut last_modified = std::time::SystemTime::now(); + let required_stable_checks = 15; // Must be stable for 30 seconds + + // Check for up to 4 hours (14400 seconds / 2 second interval = 7200 iterations) + for i in 0..7200 { + match tokio::fs::metadata(path).await { + Ok(metadata) => { + let current_size = metadata.len(); + let current_modified = metadata.modified()?; + + println!("Check {}: Size = {} bytes, Last Modified: {:?}", i, current_size, current_modified); + + if current_size > 0 { + if current_size == last_size { + // Also check if file hasn't been modified recently + if current_modified == last_modified { + stable_count += 1; + println!("Size and modification time stable for {} checks", stable_count); + + if stable_count >= required_stable_checks { + println!("File appears complete - size and modification time stable for 30 seconds"); + // Extra 30 second buffer after stability to be sure + tokio::time::sleep(Duration::from_secs(30)).await; + return Ok(()); + } + } else { + println!("File still being modified"); + stable_count = 0; + } + } else { + println!("Size changed: {} -> {}", last_size, current_size); + stable_count = 0; + } + + last_size = current_size; + last_modified = current_modified; + } + }, + Err(e) => { + println!("Error checking file: {}", e); + return Err(anyhow!("Failed to check file metadata: {}", e)); + } + } + tokio::time::sleep(Duration::from_secs(2)).await; + } + + // If we reach here, it timed out after 4 hours - something is wrong + println!("Timeout after 4 hours - file is still being written?"); + Err(anyhow!("Timeout after 4 hours waiting for file to stabilize")) + } + + pub async fn extract_date_from_filename(&self, filename: &str) -> Result { + // Example filename: "2024-12-27_18-42-36.mp4" + let date_time_str = filename + .strip_suffix(".mp4") + .ok_or_else(|| anyhow!("Invalid filename format"))?; + + // Parse the full date and time + let date = NaiveDateTime::parse_from_str(date_time_str, "%Y-%m-%d_%H-%M-%S")?; + Ok(date) + } + + pub async fn process_file(&mut self, path: PathBuf) -> Result<()> { + // Only process .mp4 files + if path.extension().and_then(|ext| ext.to_str()) != Some("mp4") { + return Err(anyhow!("Ignoring non-MP4 file")); + } + + println!("Processing livestream recording: {}", path.display()); + + // Wait for file to be fully copied + self.wait_for_file_ready(&path).await?; + + // Try to retry any cached syncs first + if let Err(e) = self.retry_cached_syncs().await { + println!("Warning: Failed to retry cached syncs: {}", e); + } + + // Sync the file to PC immediately after detection and readiness check + if let Err(e) = self.sync_to_pc(&path).await { + println!("Warning: Failed to sync file to PC: {}", e); + } + + // Get the filename + let filename = path.file_name() + .ok_or_else(|| anyhow!("Invalid filename"))? + .to_str() + .ok_or_else(|| anyhow!("Invalid UTF-8 in filename"))?; + + // Extract date from filename + let date = self.extract_date_from_filename(filename).await?; + + // Create date-based directory structure + let year_dir = self.output_path.join(date.format("%Y").to_string()); + let month_dir = year_dir.join(format!("{}-{}", + date.format("%m"), // numeric month (12) + date.format("%B") // full month name (December) + )); + + // Create directories if they don't exist + tokio::fs::create_dir_all(&month_dir).await?; + + // Check for existing files + let divine_worship_file = month_dir.join(format!( + "Divine Worship Service - RTSDA | {}.mp4", + date.format("%B %d %Y") + )); + let afternoon_program_file = month_dir.join(format!( + "Afternoon Program - RTSDA | {}.mp4", + date.format("%B %d %Y") + )); + + // Determine which filename to use + let (base_filename, nfo_title, nfo_tag) = if !divine_worship_file.exists() { + ( + format!("Divine Worship Service - RTSDA | {}", date.format("%B %d %Y")), + format!("Divine Worship Service - RTSDA | {}", date.format("%B %-d %Y")), + "Divine Worship Service" + ) + } else if !afternoon_program_file.exists() { + ( + format!("Afternoon Program - RTSDA | {}", date.format("%B %d %Y")), + format!("Afternoon Program - RTSDA | {}", date.format("%B %-d %Y")), + "Afternoon Program" + ) + } else { + // Both exist, add suffix to Afternoon Program + let mut suffix = 1; + let mut test_file = month_dir.join(format!( + "Afternoon Program - RTSDA | {} ({}).mp4", + date.format("%B %d %Y"), + suffix + )); + while test_file.exists() { + suffix += 1; + test_file = month_dir.join(format!( + "Afternoon Program - RTSDA | {} ({}).mp4", + date.format("%B %d %Y"), + suffix + )); + } + ( + format!("Afternoon Program - RTSDA | {} ({})", date.format("%B %d %Y"), suffix), + format!("Afternoon Program - RTSDA | {} ({})", date.format("%B %-d %Y"), suffix), + "Afternoon Program" + ) + }; + + let output_file = month_dir.join(format!("{}.mp4", base_filename)); + + println!("Converting to AV1 and saving to: {}", output_file.display()); + + // Build ffmpeg command for AV1 conversion using QSV + let status = Command::new("ffmpeg") + .arg("-init_hw_device").arg("qsv=hw") + .arg("-filter_hw_device").arg("hw") + .arg("-hwaccel").arg("qsv") + .arg("-hwaccel_output_format").arg("qsv") + .arg("-i").arg(&path) + .arg("-c:v").arg("av1_qsv") + .arg("-preset").arg("4") + .arg("-b:v").arg("6M") + .arg("-maxrate").arg("12M") + .arg("-bufsize").arg("24M") + .arg("-c:a").arg("copy") + .arg("-n") // Never overwrite existing files + .arg(&output_file) + .status() + .await?; + + if !status.success() { + return Err(anyhow!("FFmpeg conversion failed")); + } + + // Create NFO file + println!("Creating NFO file..."); + let nfo_content = format!(r#" + + {} + LiveStreams + {} + {} + {} + {} + {} + {} +"#, + nfo_title, + date.format("%Y").to_string(), + date.format("%m%d").to_string(), + date.format("%Y-%m-%d"), + date.format("%Y"), + date.format("%m%d"), + nfo_tag + ); + + let nfo_path = output_file.with_extension("nfo"); + tokio::fs::write(nfo_path, nfo_content).await?; + + println!("Successfully converted {} to AV1 and created NFO", path.display()); + + // Delete original file after successful processing and sync + match tokio::fs::remove_file(&path).await { + Ok(_) => println!("Successfully deleted original file: {}", path.display()), + Err(e) => println!("Warning: Failed to delete original file {}: {}", path.display(), e), + } + + Ok(()) + } +} diff --git a/package/src/services/mod.rs b/package/src/services/mod.rs new file mode 100644 index 0000000..ee0140f --- /dev/null +++ b/package/src/services/mod.rs @@ -0,0 +1 @@ +pub mod livestream_archiver; diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..60132f263d19e553c5e0ac2b8b82440794a4b936 GIT binary patch literal 6148 zcmeHKPfrs;6o11X*@DOdEfW7E8+$RKK|sV94=x2`hzAHuu!LgSZHKzDovFLqRwN`n z>j&@y_yJ5jdDIV}N3S0I0$x1po7q_btMNv}>`P{TZ|1!>Gw-*VnH>Ni`9@_LKnDN~ zi@=_KtbQZHE=q@Vsh(a$BFC5nAA%*1mmROeJLyOmNErCn7!Z564{Bh83k;6zU+$nq z91D3Yli#JFCXRvtDDLwnMwC%g`~9c?o4@%xt)T9B(*Lbf_uL?yn)*b#x_f%h^l5$C zfVO2l;w>w*!$y#|{Y}wY{{Eaqfh#(mbrJ7Nl3p6yU? zndNyH^4f;&`!=i6{9eJuEX2y?lvdp?yR4eM^EF~Kl{K$Mnhx2RU@aYM3Q6`I-IjW4 zjd4b^=1OGAn)j891`{w378KzLwBaSZg4ggKKEfCHM!Lx$86#K71j&&bWM+3aO^jfpQ*4NVH6HT z&Wz)znTdL#5IH;c8`2$!8DUZ<3?vL3XP`T3>f-*t`{(!n@t~xYFpx0tUok*>7Oh3` z%vX17M{?q>HLz@B5hDEB5y}v()O9QsaTSkak%BfuD2S%Qc7)i2lKcor8j>nu;6xet E0Z*moCjbBd literal 0 HcmV?d00001 diff --git a/src/main.rs b/src/main.rs index 26c33f7..2b3852a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use services::livestream_archiver::LivestreamArchiver; #[tokio::main] async fn main() -> Result<()> { - let watch_path = PathBuf::from("/home/rockvilleav/Sync/Livestreams"); + let watch_path = PathBuf::from("/mnt/sync/Livestreams"); let output_path = PathBuf::from("/media/archive/jellyfin/livestreams"); // Ensure directories exist @@ -25,7 +25,13 @@ async fn main() -> Result<()> { println!("Watching directory: {}", watch_path.display()); println!("Output directory: {}", output_path.display()); - let archiver = LivestreamArchiver::new(output_path.clone()); + // Configure PC sync target (replace with your actual PC address and path) + let pc_sync_target = std::env::var("PC_SYNC_TARGET") + .unwrap_or_else(|_| "benjaminslingo@macbook-pro.slingoapps.dev:~/rtsda/livestreams/".to_string()); + + let archiver = Arc::new(Mutex::new( + LivestreamArchiver::with_pc_sync(output_path.clone(), pc_sync_target) + )); let processed_files = Arc::new(Mutex::new(HashSet::new())); // Process existing files first @@ -38,9 +44,10 @@ async fn main() -> Result<()> { if path.extension().and_then(|ext| ext.to_str()) == Some("mp4") { // Extract date from filename to check if output exists if let Some(filename) = path.file_name().and_then(|f| f.to_str()) { - if let Ok(date) = archiver.extract_date_from_filename(filename).await { + let archiver_guard = archiver.lock().unwrap(); + if let Ok(date) = archiver_guard.extract_date_from_filename(filename).await { // Check if either Divine Worship or Afternoon Program exists for this date - let year_dir = archiver.get_output_path().join(date.format("%Y").to_string()); + let year_dir = archiver_guard.get_output_path().join(date.format("%Y").to_string()); let month_dir = year_dir.join(format!("{}-{}", date.format("%m"), date.format("%B") @@ -57,7 +64,9 @@ async fn main() -> Result<()> { if !divine_worship_file.exists() && !afternoon_program_file.exists() { println!("Found unprocessed file: {}", path.display()); - if let Err(e) = archiver.process_file(path).await { + drop(archiver_guard); // Release lock before async operation + let mut archiver_mut = archiver.lock().unwrap(); + if let Err(e) = archiver_mut.process_file(path).await { eprintln!("Error processing existing file: {}", e); } } else { @@ -96,13 +105,16 @@ async fn main() -> Result<()> { for path in event.paths { if let Ok(canonical_path) = std::fs::canonicalize(&path) { let path_str = canonical_path.to_string_lossy().to_string(); - let mut processed = processed_files.lock().unwrap(); + let processed = processed_files.lock().unwrap(); if !processed.contains(&path_str) { println!("Processing file: {}", path_str); - if let Err(e) = archiver.process_file(path).await { + drop(processed); // Release processed files lock + let mut archiver_mut = archiver.lock().unwrap(); + if let Err(e) = archiver_mut.process_file(path).await { eprintln!("Error processing file: {}", e); } else { + let mut processed = processed_files.lock().unwrap(); processed.insert(path_str); if processed.len() > 1000 { processed.clear(); diff --git a/src/services/livestream_archiver.rs b/src/services/livestream_archiver.rs index df2cd57..b28f0cb 100644 --- a/src/services/livestream_archiver.rs +++ b/src/services/livestream_archiver.rs @@ -3,21 +3,95 @@ use anyhow::{Result, anyhow}; use chrono::NaiveDateTime; use tokio::process::Command; use tokio::time::Duration; +use std::collections::VecDeque; pub struct LivestreamArchiver { output_path: PathBuf, + pc_sync_target: Option, + sync_cache: VecDeque, } impl LivestreamArchiver { pub fn new(output_path: PathBuf) -> Self { LivestreamArchiver { output_path, + pc_sync_target: None, + sync_cache: VecDeque::new(), + } + } + + pub fn with_pc_sync(output_path: PathBuf, pc_sync_target: String) -> Self { + LivestreamArchiver { + output_path, + pc_sync_target: Some(pc_sync_target), + sync_cache: VecDeque::new(), } } pub fn get_output_path(&self) -> &PathBuf { &self.output_path } + + async fn sync_to_pc(&mut self, file_path: &PathBuf) -> Result<()> { + if let Some(target) = &self.pc_sync_target { + println!("Syncing {} to PC at {}", file_path.display(), target); + + let status = Command::new("rsync") + .arg("-avz") + .arg("--progress") + .arg("-e") + .arg("ssh -p 8443") + .arg(file_path) + .arg(target) + .status() + .await?; + + if status.success() { + println!("Successfully synced {} to PC", file_path.display()); + Ok(()) + } else { + println!("Failed to sync {} to PC, adding to cache", file_path.display()); + self.sync_cache.push_back(file_path.clone()); + Err(anyhow!("Rsync failed")) + } + } else { + println!("No PC sync target configured, skipping sync"); + Ok(()) + } + } + + async fn retry_cached_syncs(&mut self) -> Result<()> { + if let Some(target) = &self.pc_sync_target { + let mut successful_syncs = Vec::new(); + + for (index, file_path) in self.sync_cache.iter().enumerate() { + println!("Retrying sync for cached file: {}", file_path.display()); + + let status = Command::new("rsync") + .arg("-avz") + .arg("--progress") + .arg("-e") + .arg("ssh -p 8443") + .arg(file_path) + .arg(target) + .status() + .await?; + + if status.success() { + println!("Successfully synced cached file: {}", file_path.display()); + successful_syncs.push(index); + } else { + println!("Still failed to sync: {}", file_path.display()); + } + } + + // Remove successfully synced files from cache (in reverse order to maintain indices) + for &index in successful_syncs.iter().rev() { + self.sync_cache.remove(index); + } + } + Ok(()) + } async fn wait_for_file_ready(&self, path: &PathBuf) -> Result<()> { println!("Waiting for file to be ready: {}", path.display()); @@ -89,7 +163,7 @@ impl LivestreamArchiver { Ok(date) } - pub async fn process_file(&self, path: PathBuf) -> Result<()> { + pub async fn process_file(&mut self, path: PathBuf) -> Result<()> { // Only process .mp4 files if path.extension().and_then(|ext| ext.to_str()) != Some("mp4") { return Err(anyhow!("Ignoring non-MP4 file")); @@ -97,8 +171,15 @@ impl LivestreamArchiver { println!("Processing livestream recording: {}", path.display()); - // Wait for file to be fully copied - self.wait_for_file_ready(&path).await?; + // Try to retry any cached syncs first + if let Err(e) = self.retry_cached_syncs().await { + println!("Warning: Failed to retry cached syncs: {}", e); + } + + // Sync the file to PC immediately after detection and readiness check + if let Err(e) = self.sync_to_pc(&path).await { + println!("Warning: Failed to sync file to PC: {}", e); + } // Get the filename let filename = path.file_name() @@ -218,8 +299,11 @@ impl LivestreamArchiver { println!("Successfully converted {} to AV1 and created NFO", path.display()); - // Don't delete original file - println!("Original file preserved at: {}", path.display()); + // Delete original file after successful processing and sync + match tokio::fs::remove_file(&path).await { + Ok(_) => println!("Successfully deleted original file: {}", path.display()), + Err(e) => println!("Warning: Failed to delete original file {}: {}", path.display(), e), + } Ok(()) }