diff --git a/.gitignore b/.gitignore index 17cc106..eff8c52 100755 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ surf.sublime-workspace /target maple.sublime-workspace +*.secret.yaml diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..62e5634 --- /dev/null +++ b/CLAUDE.md @@ -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 +``` + +### 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 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0358936..043c79a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 15033fe..97ccd34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/etc/k8s/maple.seal.yaml b/etc/k8s/maple.seal.yaml new file mode 100644 index 0000000..25c2847 --- /dev/null +++ b/etc/k8s/maple.seal.yaml @@ -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 diff --git a/etc/k8s/seaweed.yaml b/etc/k8s/seaweed.yaml new file mode 100644 index 0000000..cb4eb02 --- /dev/null +++ b/etc/k8s/seaweed.yaml @@ -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 \ No newline at end of file diff --git a/etc/run_maple.sh b/etc/run_maple.sh index 0d4a94b..c09333b 100644 --- a/etc/run_maple.sh +++ b/etc/run_maple.sh @@ -22,8 +22,11 @@ configure() { } start() { - # sleep 100000 -} + echo "waiting for input..." + while [[ true ]]; do + sleep 10000000 + done + } main() { directories diff --git a/maple.Dockerfile b/maple.Dockerfile index e5c1f1f..ea907e6 100644 --- a/maple.Dockerfile +++ b/maple.Dockerfile @@ -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 diff --git a/src/main.rs b/src/main.rs index e7a11a9..c653b43 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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> { + 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(()) } diff --git a/src/playlists.rs b/src/playlists.rs new file mode 100644 index 0000000..ebd38eb --- /dev/null +++ b/src/playlists.rs @@ -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); + } + } +} \ No newline at end of file