Compare commits
No commits in common. "2c3c86e07d4c5da4e7f7dd43d5121dfccc255ed5" and "e778e0ec8fe9629a83169659fe5e9122280a26b3" have entirely different histories.
2c3c86e07d
...
e778e0ec8f
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1 @@
|
||||||
/target
|
/target
|
||||||
*.mp4
|
|
||||||
|
|
104
DEPLOY.md
104
DEPLOY.md
|
@ -1,104 +0,0 @@
|
||||||
# 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`
|
|
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Rockville Tolland Seventh-day Adventist Church
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
130
README.md
130
README.md
|
@ -1,130 +0,0 @@
|
||||||
# Livestream Archiver
|
|
||||||
|
|
||||||
A Rust application that monitors a directory for livestream recordings, processes them, and syncs them to a remote server. Designed for automated livestream archiving with hardware-accelerated transcoding.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **File Detection**: Monitors a configured directory for new MP4 files
|
|
||||||
- **Readiness Check**: Waits for files to be completely written before processing
|
|
||||||
- **Remote Sync**: Uses rsync to send files to a remote server 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
|
|
||||||
- **Systemd Integration**: Runs as a system service with automatic restart
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Rust toolchain (1.70 or later)
|
|
||||||
- `rsync` for file synchronization
|
|
||||||
- `ffmpeg` compiled with Intel QSV support
|
|
||||||
- SSH key authentication configured for passwordless rsync
|
|
||||||
|
|
||||||
### Building from Source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the repository
|
|
||||||
git clone <repository-url>
|
|
||||||
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:
|
|
||||||
```
|
|
||||||
<output-directory>/
|
|
||||||
├── 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.
|
|
87
deploy.sh
87
deploy.sh
|
@ -1,87 +0,0 @@
|
||||||
#!/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!"
|
|
Binary file not shown.
|
@ -1,16 +0,0 @@
|
||||||
[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
|
|
BIN
output.mp4
Normal file
BIN
output.mp4
Normal file
Binary file not shown.
94
package.sh
94
package.sh
|
@ -1,94 +0,0 @@
|
||||||
#!/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"
|
|
748
package/Cargo.lock
generated
748
package/Cargo.lock
generated
|
@ -1,748 +0,0 @@
|
||||||
# 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"
|
|
|
@ -1,14 +0,0 @@
|
||||||
[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
|
|
|
@ -1,104 +0,0 @@
|
||||||
# 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`
|
|
|
@ -1,59 +0,0 @@
|
||||||
# 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
|
|
||||||
│ │ └── ...
|
|
||||||
│ └── ...
|
|
||||||
└── ...
|
|
||||||
```
|
|
Binary file not shown.
|
@ -1,16 +0,0 @@
|
||||||
[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
|
|
|
@ -1,134 +0,0 @@
|
||||||
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<Event, notify::Error>| {
|
|
||||||
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(())
|
|
||||||
}
|
|
|
@ -1,313 +0,0 @@
|
||||||
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<String>,
|
|
||||||
sync_cache: VecDeque<PathBuf>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<NaiveDateTime> {
|
|
||||||
// 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#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<episodedetails>
|
|
||||||
<title>{}</title>
|
|
||||||
<showtitle>LiveStreams</showtitle>
|
|
||||||
<season>{}</season>
|
|
||||||
<episode>{}</episode>
|
|
||||||
<aired>{}</aired>
|
|
||||||
<displayseason>{}</displayseason>
|
|
||||||
<displayepisode>{}</displayepisode>
|
|
||||||
<tag>{}</tag>
|
|
||||||
</episodedetails>"#,
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod livestream_archiver;
|
|
BIN
src/.DS_Store
vendored
BIN
src/.DS_Store
vendored
Binary file not shown.
54
src/main.rs
54
src/main.rs
|
@ -10,7 +10,7 @@ use services::livestream_archiver::LivestreamArchiver;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let watch_path = PathBuf::from("/mnt/sync/Livestreams");
|
let watch_path = PathBuf::from("/home/rockvilleav/Sync/Livestreams");
|
||||||
let output_path = PathBuf::from("/media/archive/jellyfin/livestreams");
|
let output_path = PathBuf::from("/media/archive/jellyfin/livestreams");
|
||||||
|
|
||||||
// Ensure directories exist
|
// Ensure directories exist
|
||||||
|
@ -25,13 +25,7 @@ async fn main() -> Result<()> {
|
||||||
println!("Watching directory: {}", watch_path.display());
|
println!("Watching directory: {}", watch_path.display());
|
||||||
println!("Output directory: {}", output_path.display());
|
println!("Output directory: {}", output_path.display());
|
||||||
|
|
||||||
// Configure PC sync target (replace with your actual PC address and path)
|
let archiver = LivestreamArchiver::new(output_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()));
|
let processed_files = Arc::new(Mutex::new(HashSet::new()));
|
||||||
|
|
||||||
// Process existing files first
|
// Process existing files first
|
||||||
|
@ -40,40 +34,9 @@ async fn main() -> Result<()> {
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
if let Ok(entry) = entry {
|
if let Ok(entry) = entry {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
// Only process .mp4 files
|
println!("Found existing file: {}", path.display());
|
||||||
if path.extension().and_then(|ext| ext.to_str()) == Some("mp4") {
|
if let Err(e) = archiver.process_file(path).await {
|
||||||
// Extract date from filename to check if output exists
|
eprintln!("Error processing existing file: {}", e);
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,16 +68,13 @@ async fn main() -> Result<()> {
|
||||||
for path in event.paths {
|
for path in event.paths {
|
||||||
if let Ok(canonical_path) = std::fs::canonicalize(&path) {
|
if let Ok(canonical_path) = std::fs::canonicalize(&path) {
|
||||||
let path_str = canonical_path.to_string_lossy().to_string();
|
let path_str = canonical_path.to_string_lossy().to_string();
|
||||||
let processed = processed_files.lock().unwrap();
|
let mut processed = processed_files.lock().unwrap();
|
||||||
|
|
||||||
if !processed.contains(&path_str) {
|
if !processed.contains(&path_str) {
|
||||||
println!("Processing file: {}", path_str);
|
println!("Processing file: {}", path_str);
|
||||||
drop(processed); // Release processed files lock
|
if let Err(e) = archiver.process_file(path).await {
|
||||||
let mut archiver_mut = archiver.lock().unwrap();
|
|
||||||
if let Err(e) = archiver_mut.process_file(path).await {
|
|
||||||
eprintln!("Error processing file: {}", e);
|
eprintln!("Error processing file: {}", e);
|
||||||
} else {
|
} else {
|
||||||
let mut processed = processed_files.lock().unwrap();
|
|
||||||
processed.insert(path_str);
|
processed.insert(path_str);
|
||||||
if processed.len() > 1000 {
|
if processed.len() > 1000 {
|
||||||
processed.clear();
|
processed.clear();
|
||||||
|
|
|
@ -3,96 +3,18 @@ use anyhow::{Result, anyhow};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::time::Duration;
|
use tokio::time::Duration;
|
||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
pub struct LivestreamArchiver {
|
pub struct LivestreamArchiver {
|
||||||
output_path: PathBuf,
|
output_path: PathBuf,
|
||||||
pc_sync_target: Option<String>,
|
|
||||||
sync_cache: VecDeque<PathBuf>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LivestreamArchiver {
|
impl LivestreamArchiver {
|
||||||
pub fn new(output_path: PathBuf) -> Self {
|
pub fn new(output_path: PathBuf) -> Self {
|
||||||
LivestreamArchiver {
|
LivestreamArchiver {
|
||||||
output_path,
|
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<()> {
|
async fn wait_for_file_ready(&self, path: &PathBuf) -> Result<()> {
|
||||||
println!("Waiting for file to be ready: {}", path.display());
|
println!("Waiting for file to be ready: {}", path.display());
|
||||||
|
|
||||||
|
@ -152,7 +74,7 @@ impl LivestreamArchiver {
|
||||||
Err(anyhow!("Timeout after 4 hours waiting for file to stabilize"))
|
Err(anyhow!("Timeout after 4 hours waiting for file to stabilize"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn extract_date_from_filename(&self, filename: &str) -> Result<NaiveDateTime> {
|
async fn extract_date_from_filename(&self, filename: &str) -> Result<NaiveDateTime> {
|
||||||
// Example filename: "2024-12-27_18-42-36.mp4"
|
// Example filename: "2024-12-27_18-42-36.mp4"
|
||||||
let date_time_str = filename
|
let date_time_str = filename
|
||||||
.strip_suffix(".mp4")
|
.strip_suffix(".mp4")
|
||||||
|
@ -163,7 +85,38 @@ impl LivestreamArchiver {
|
||||||
Ok(date)
|
Ok(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn process_file(&mut self, path: PathBuf) -> Result<()> {
|
async fn create_nfo_file(&self, video_path: &PathBuf, date: &NaiveDateTime) -> Result<()> {
|
||||||
|
let nfo_path = video_path.with_extension("nfo");
|
||||||
|
|
||||||
|
// Format the full title with date including year
|
||||||
|
let full_title = format!("Divine Worship Service - RTSDA | {}",
|
||||||
|
date.format("%B %-d %Y") // Format like "December 28 2024"
|
||||||
|
);
|
||||||
|
|
||||||
|
let nfo_content = format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<episodedetails>
|
||||||
|
<title>{}</title>
|
||||||
|
<showtitle>LiveStreams</showtitle>
|
||||||
|
<season>{}</season>
|
||||||
|
<episode>{}</episode>
|
||||||
|
<aired>{}</aired>
|
||||||
|
<displayseason>{}</displayseason>
|
||||||
|
<displayepisode>{}</displayepisode>
|
||||||
|
<tag>Divine Worship Service</tag>
|
||||||
|
</episodedetails>"#,
|
||||||
|
full_title,
|
||||||
|
date.format("%Y").to_string(),
|
||||||
|
date.format("%m%d").to_string(),
|
||||||
|
date.format("%Y-%m-%d"),
|
||||||
|
date.format("%Y"),
|
||||||
|
date.format("%m%d")
|
||||||
|
);
|
||||||
|
|
||||||
|
tokio::fs::write(nfo_path, nfo_content).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn process_file(&self, path: PathBuf) -> Result<()> {
|
||||||
// Only process .mp4 files
|
// Only process .mp4 files
|
||||||
if path.extension().and_then(|ext| ext.to_str()) != Some("mp4") {
|
if path.extension().and_then(|ext| ext.to_str()) != Some("mp4") {
|
||||||
return Err(anyhow!("Ignoring non-MP4 file"));
|
return Err(anyhow!("Ignoring non-MP4 file"));
|
||||||
|
@ -171,15 +124,8 @@ impl LivestreamArchiver {
|
||||||
|
|
||||||
println!("Processing livestream recording: {}", path.display());
|
println!("Processing livestream recording: {}", path.display());
|
||||||
|
|
||||||
// Try to retry any cached syncs first
|
// Wait for file to be fully copied
|
||||||
if let Err(e) = self.retry_cached_syncs().await {
|
self.wait_for_file_ready(&path).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
|
// Get the filename
|
||||||
let filename = path.file_name()
|
let filename = path.file_name()
|
||||||
|
@ -200,53 +146,12 @@ impl LivestreamArchiver {
|
||||||
// Create directories if they don't exist
|
// Create directories if they don't exist
|
||||||
tokio::fs::create_dir_all(&month_dir).await?;
|
tokio::fs::create_dir_all(&month_dir).await?;
|
||||||
|
|
||||||
// Check for existing files
|
// Create output path with .mp4 extension
|
||||||
let divine_worship_file = month_dir.join(format!(
|
let output_file = month_dir.join(format!(
|
||||||
"Divine Worship Service - RTSDA | {}.mp4",
|
"Divine Worship Service - RTSDA | {}{}",
|
||||||
date.format("%B %d %Y")
|
date.format("%B %d %Y"),
|
||||||
|
".mp4"
|
||||||
));
|
));
|
||||||
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());
|
println!("Converting to AV1 and saving to: {}", output_file.display());
|
||||||
|
|
||||||
|
@ -263,7 +168,7 @@ impl LivestreamArchiver {
|
||||||
.arg("-maxrate").arg("12M")
|
.arg("-maxrate").arg("12M")
|
||||||
.arg("-bufsize").arg("24M")
|
.arg("-bufsize").arg("24M")
|
||||||
.arg("-c:a").arg("copy")
|
.arg("-c:a").arg("copy")
|
||||||
.arg("-n") // Never overwrite existing files
|
.arg("-y")
|
||||||
.arg(&output_file)
|
.arg(&output_file)
|
||||||
.status()
|
.status()
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -274,36 +179,12 @@ impl LivestreamArchiver {
|
||||||
|
|
||||||
// Create NFO file
|
// Create NFO file
|
||||||
println!("Creating NFO file...");
|
println!("Creating NFO file...");
|
||||||
let nfo_content = format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
self.create_nfo_file(&output_file, &date).await?;
|
||||||
<episodedetails>
|
|
||||||
<title>{}</title>
|
|
||||||
<showtitle>LiveStreams</showtitle>
|
|
||||||
<season>{}</season>
|
|
||||||
<episode>{}</episode>
|
|
||||||
<aired>{}</aired>
|
|
||||||
<displayseason>{}</displayseason>
|
|
||||||
<displayepisode>{}</displayepisode>
|
|
||||||
<tag>{}</tag>
|
|
||||||
</episodedetails>"#,
|
|
||||||
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());
|
println!("Successfully converted {} to AV1 and created NFO", path.display());
|
||||||
|
|
||||||
// Delete original file after successful processing and sync
|
// Don't delete original file
|
||||||
match tokio::fs::remove_file(&path).await {
|
println!("Original file preserved at: {}", path.display());
|
||||||
Ok(_) => println!("Successfully deleted original file: {}", path.display()),
|
|
||||||
Err(e) => println!("Warning: Failed to delete original file {}: {}", path.display(), e),
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue