This commit is contained in:
Nathan Rashleigh committed 2025-08-31 19:18:34 +10:00
1 parent a6c208eb46
commit fcec3b8c36
10 files changed
+440 -3

No files matched your search

+1
View File
@@ -15,3 +15,4 @@ surf.sublime-workspace
/target
maple.sublime-workspace
*.secret.yaml
+85
View File
@@ -0,0 +1,85 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is "maple" - a music collection management system built in Rust that orchestrates downloading music from Soulseek using slsk-batchdl (sldl) and serving files through slskd. The project uses Docker containers to run two main services: a Soulseek downloader/organizer and a Soulseek daemon for sharing.
## Architecture
The project consists of:
- **Rust application** (`src/main.rs`): Currently minimal, likely intended for future music management logic
- **slsk-batchdl integration**: Downloads music from Soulseek with organized file structure
- **slskd daemon**: Provides web interface and sharing capabilities for Soulseek
- **Docker orchestration**: Two containers managed via docker-compose
### Key Components
- `maple` service: Runs the Rust app and sldl downloader
- `slskd` service: Runs the Soulseek daemon with web interface on port 5030
- Shared data directory structure under `data/` for music files and metadata
- Configuration templates in `etc/` that use environment variable substitution
## Common Commands
### Development and Building
```bash
# Build containers
just build
# Start all services
just up
# Stop all services
just down
# Run interactive shell in maple container
just run
# Execute command in running maple container
just exec
# Download music using sldl
just dl <search_target>
```
### Docker Operations
```bash
# Manual container operations
docker compose -f compose.yaml up --remove-orphans
docker compose -f compose.yaml down -v --remove-orphans
docker compose -f compose.yaml build
```
### Rust Development
```bash
# Build Rust code
cargo build
# Run locally (outside container)
cargo run
```
## Configuration
- Environment variables are loaded from `.env` file
- Required variables: `SLDL_USER`, `SLDL_PASS` for Soulseek authentication
- sldl configuration template: `etc/sldl.tpl.conf`
- slskd configuration template: `etc/slskd.tpl.yml`
## File Organization
Music files are organized as: `{artist}/{album}/{track} {title}` with fallback to `_missing_tags/` for untagged files. The system prefers FLAC and MP3 formats with bitrate range 200-2500 kbps.
## Data Structure
- `data/maple/`: Organized music collection by artist/album
- `data/_metadata/`: Index files and metadata
- `data/slskd/`: Soulseek daemon data (downloads, shared files)
- `data/PIONEER/`: DJ software settings and playlists
## Current Development Status
The Rust application is in early development (currently just prints "Hello, world!"). The infrastructure for downloading and organizing music is operational through the Docker services.
- use an indentation of 4 spaces
Generated
+117
View File
@@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -124,6 +130,12 @@ version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.33"
@@ -211,12 +223,37 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "flate2"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "glob"
version = "0.3.3"
@@ -275,6 +312,32 @@ version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "lofty"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8bc4717ff10833a623b009e9254ae8667c7a59edc3cfb01c37aeeef4b6d54a7"
dependencies = [
"byteorder",
"data-encoding",
"flate2",
"lofty_attr",
"log",
"ogg_pager",
"paste",
]
[[package]]
name = "lofty_attr"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9983e64b2358522f745c1251924e3ab7252d55637e80f6a0a3de642d6a9efc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "log"
version = "0.4.27"
@@ -285,7 +348,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
name = "maple"
version = "0.1.0"
dependencies = [
"lofty",
"rekordcrate",
"walkdir",
]
[[package]]
@@ -294,6 +359,15 @@ version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "modular-bitfield"
version = "0.11.2"
@@ -324,6 +398,15 @@ dependencies = [
"autocfg",
]
[[package]]
name = "ogg_pager"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87b0bef808533c5890ab77279538212efdbbbd9aa4ef1ccdfcfbf77a42f7e6fa"
dependencies = [
"byteorder",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -367,6 +450,12 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "proc-macro2"
version = "1.0.101"
@@ -448,6 +537,15 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[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 = "serde"
version = "1.0.219"
@@ -563,6 +661,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[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 = "wasm-bindgen"
version = "0.2.100"
@@ -621,6 +729,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "winapi-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-core"
version = "0.61.2"
+10
View File
@@ -5,3 +5,13 @@ edition = "2024"
[dependencies]
rekordcrate = "0.3.0"
lofty = "0.21"
walkdir = "2.4"
[[bin]]
name = "playlists"
path = "src/playlists.rs"
[[bin]]
name = "library"
path = "src/main.rs"
+19
View File
@@ -0,0 +1,19 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: maple-s3
namespace: maple
spec:
encryptedData:
AWS_ACCESS_KEY_ID: AgA+7VW1EbhJvQcE6kfY/RBR1duESF5H8jK3hKv2vZr7JgZ0/Z8h+k7eoJ09uSLaOIVSa/ELiT4PNoN8UXcfY5IeKNTqNo0onYncaosXelZUWUWLCtkciGHTcEUWeZJXl3Td+17PWG3cyl3vpki8eCRT9V3nF7166P34Dd0qU+kC85hRsDdMzOGlRAkLWtlEILIXfXNKqecYiNj0jf33uVAaNpEkC7itEUvtHl63s7fjmeaoAGRp/nG7nN6hSc5BGaiEROGAkgL9C/1I51sS5Z8cs1bW48YtUrlm/oP8aOIza4aEYp/p1V3+Zc+SMSBsgeJoib09EQ8zLsXvHLfeLOeca8ae9AvM7vtswmHbHI6b+kNc27+kiALRSVdnDRJRnTfS2yO/PHmS5fbV6NBdJvrMCJsCDV97XMUBB0AY2950+agD82oRJpaTcJ/lTai6kowKncLGaheeAm9yuqxieWY4DWpuSz7mlWnI9XMeEL5b5Knpg00WewISIl38qDVICymhtprqNwFVfkXQoqSdMdithidBLDZTyMczCrzHMRpOvq2iyxeYdQ2GQunybZBVIZq3P1wvLi8eIXC19T1yiwQw85o1uS0aedO720Anbl4Ciy3Xup9tfmRGPla4t7WKgFCpYo5CQdTdxpSEOD/Fi7yuovgUhORgYiM0/y03k4H5fw/DHZupEXRco7y0fA7jCr84q5lFy7TAlIXbXvOIXUUjj4fHSUbKf6ZAAZdroHC+gw==
AWS_SECRET_ACCESS_KEY: AgDNSebhOjXWgjN+JS1ou1Jy6j9A1DOpGtOFgdivTmx8ZyXpZ/bYaQ+C9il4qrCVeLVLG+e8zcdZ2siErB0GQkuhHN/DBtYnmuf6ki19Vihd+KVrYrm7Q5ZgeWe74XUv/5qrKBUe2Kp/uM29pc/IOC0fMnF+K9Z/M6U8mmtiMGE1Q12+X2Zo56YDTLymbIjLkPO0ywCZ0Vl6BIwmWcTBLfc0wGV2HwEH50uee9a0uvIoOXk9r9olkBQjAFc+wzctMBQMc0kpPDimE9PbqH0k8VPjWnBmRStILqQ825Cqt4KNTq3xamWSOkOpO2Yb/QZL2xHh0P/ErLnQ0gmCs4CVgggSuFYMkyan06DmYlYifkmmbkqWe7FivaJ1HAchpQM9TRarqBGnhC4k5bwrDmpfVyajHezCNnD+KjphO2qy361/t/Nl91a7UDq/IKQlyDVQSwyWJs1oC4zYvcSikCd/XD9VjVsiBVQoKSMGWhNhHZkHVJ4f3nmDIGi/WNNV51LbbRqdl+TmT0brIEzUqMRMnzxaDjrBAfsUB8i5REa8SI2c+1zo4JVV+6fjKTc3dYvAiYWjpGVRkzA4W5hypSroGv/g5lVevQxQqqCXBNCIuXwbNM6L9j5sT2irQgh8Ph4DKlpQxc5skQSwGob2KseDONofy84mzp9h5Dtg4HxNeKTwPjjqgnigHK2urGOJ2atvuRG7HIWpdFdRVbqkOX9GPEPp1i19cOubkYOaioz0U4di0A==
template:
metadata:
creationTimestamp: null
labels:
app: maple
name: maple-s3
namespace: maple
type: Opaque
+108
View File
@@ -0,0 +1,108 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: maple
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: maple-data
namespace: maple
spec:
storageClassName: zfs-fast
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: seaweedfs
namespace: maple
spec:
replicas: 1
selector:
matchLabels:
app: seaweedfs
template:
metadata:
labels:
app: seaweedfs
spec:
containers:
- name: seaweedfs
image: chrislusf/seaweedfs:latest
args:
- "server"
- "-s3"
- "-dir=/data/maple"
env:
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: maple-s3
key: AWS_ACCESS_KEY_ID
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: maple-s3
key: AWS_SECRET_ACCESS_KEY
ports:
- containerPort: 8333
- containerPort: 9333
- containerPort: 8080
- containerPort: 8888
volumeMounts:
- name: maple-data
mountPath: /data/maple
volumes:
- name: maple-data
persistentVolumeClaim:
claimName: maple-data
---
apiVersion: v1
kind: Service
metadata:
name: seaweedfs
namespace: maple
spec:
selector:
app: seaweedfs
ports:
- name: s3
port: 8333
targetPort: 8333
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: seaweedfs
namespace: maple
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: haproxy
tls:
- hosts:
- maple.ntwl.xyz
secretName: maple-seaweedfs-tls
rules:
- host: maple.ntwl.xyz
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: seaweedfs
port:
number: 8333
+5 -2
View File
@@ -22,8 +22,11 @@ configure() {
}
start() {
# sleep 100000
}
echo "waiting for input..."
while [[ true ]]; do
sleep 10000000
done
}
main() {
directories
+9
View File
@@ -1,6 +1,7 @@
FROM debian:bookworm
ENV SLSK_BATCHDL_V "2.4.7"
ENV S5CMD_V "2.3.0"
RUN apt-get update
RUN apt-get install -y \
@@ -17,6 +18,14 @@ RUN curl -L \
RUN unzip sldl_linux-x64.zip
RUN chmod +x sldl
RUN mv sldl /usr/local/bin/
RUN curl -L \
"https://github.com/peak/s5cmd/releases/download/v${S5CMD_V}/s5cmd_${S5CMD_V}_Linux-64bit.tar.gz" \
-o s5cmd.tar.gz
RUN tar -xzf s5cmd.tar.gz
RUN chmod +x s5cmd
RUN mv s5cmd /usr/local/bin/
RUN rm -rf /tmp/setup
COPY ./src/ /app/src
+61 -1
View File
@@ -1,3 +1,63 @@
use lofty::read_from_path;
use lofty::prelude::*;
use std::path::Path;
use walkdir::WalkDir;
fn main() {
println!("Hello, world!");
let music_path = "data/maple";
if !Path::new(music_path).exists() {
println!("Music directory {} not found", music_path);
return;
}
println!("Scanning music files in {}...\n", music_path);
for entry in WalkDir::new(music_path)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
// Skip _missing_tags directory
if path.to_string_lossy().contains("_missing_tags") {
continue;
}
if let Some(extension) = path.extension() {
let ext = extension.to_string_lossy().to_lowercase();
if ext == "mp3" || ext == "flac" {
if let Err(e) = print_file_metadata(path) {
println!("Error reading {}: {}", path.display(), e);
}
println!();
}
}
}
}
fn print_file_metadata(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
let tagged_file = read_from_path(path)?;
println!("File: {}", path.display());
if let Some(tag) = tagged_file.primary_tag() {
if let Some(title) = tag.title() { println!(" Title: {}", title); }
if let Some(artist) = tag.artist() { println!(" Artist: {}", artist); }
if let Some(album) = tag.album() { println!(" Album: {}", album); }
if let Some(year) = tag.year() { println!(" Year: {}", year); }
if let Some(track) = tag.track() { println!(" Track: {}", track); }
if let Some(genre) = tag.genre() { println!(" Genre: {}", genre); }
} else {
println!(" No metadata found");
}
let properties = tagged_file.properties();
println!(" Duration: {}:{:02}",
properties.duration().as_secs() / 60,
properties.duration().as_secs() % 60);
println!(" Bitrate: {} kbps", properties.overall_bitrate().unwrap_or(0));
Ok(())
}
+25
View File
@@ -0,0 +1,25 @@
use std::path::Path;
fn main() {
let pdb_path = "data/PIONEER/rekordbox/export.pdb";
if !Path::new(pdb_path).exists() {
println!("Pioneer database {} not found", pdb_path);
return;
}
match rekordcrate::parse_pdb(pdb_path) {
Ok(database) => {
println!("Playlists in {}:\n", pdb_path);
for playlist in &database.playlists {
println!(" {}", playlist.name);
}
println!("\nTotal playlists: {}", database.playlists.len());
}
Err(e) => {
println!("Error reading Pioneer database: {}", e);
}
}
}