A few months ago I set my music free with an instance of the Navidrome Music Streaming Server on Pikapod. It was very easy. I was up and running in no time.
There was an old laptop that I could have used as a server but was put off by the thought of wrangling network settings. I had my fill of that with a Nextcloud instance on a Raspberry Pie some years ago.
Pikapod provides a great service at a good price and profit shares with the developers of the open source apps it hosts. All good. I was happy with the service.
Some time after I learned about Tailscale. It basically removes the pain of configuring a secure network to facilitate remote access to a local server. For my use case it’s free. What’s not to like?
What with an old unused laptop lying around, knowing I could do this and with a bit of time on my hands I decided to go for it. Guided by an LLM it was all quite straight forward. As I am writing this post the process of syncing several hundred GBs of music files to the local server is completing in the background. All working as expected. Streaming on local and remote networks.
Happy days. I’m now one of the increasing number of people whose hosting their own music streaming service.
Here’s a few details -
Server
- Model: Lenovo IdeaPad Flex 15D
- CPU: Intel Atom x7-Z8750 (4 cores, 1.6-2.56 GHz)
- RAM: 4GB
- OS: Ubuntu Server 24.04 LTS
CPU Constraints
- No real-time transcoding - CPU will struggle with FLAC→MP3 conversion
- Solution: Sync only MP3 files, pre-convert lossless formats
- Limited concurrent streams - Max 2-3 simultaneous users
- Power efficient: 2W TDP, suitable for 24/7 operation
rclone Filter File
# MP3-only filter (optimized for Atom processor - no transcoding needed)
# Include MP3 files ONLY
+ *.mp3
+ *.MP3
# Include album artwork
+ *.jpg
+ *.jpeg
+ *.png
+ *.JPG
+ *.JPEG
+ *.PNG
+ cover.*
+ folder.*
+ Cover.*
+ Folder.*
# Include playlist files
+ *.m3u
+ *.m3u8
+ *.M3U
# Exclude ALL other audio formats (prevent transcoding load)
- *.flac
- *.FLAC
- *.m4a
- *.M4A
- *.aac
- *.AAC
- *.ogg
- *.OGG
- *.opus
- *.OPUS
- *.wav
- *.WAV
- *.wma
- *.WMA
- *.ape
- *.alac
- *.aiff
# Exclude system files
- .DS_Store
- Thumbs.db
- desktop.ini
- .directory
# Exclude temporary/hidden files
- *.tmp
- *.temp
- *~
- .~*
- .*
# Exclude text/documentation
- *.txt
- *.pdf
- *.doc
- *.docx
- *.nfo
- *.log
- *.cue
# Include directories
+ */Sync Script
1#!/bin/bash
2
3################################################################################
4# Navidrome Local Server Music Sync Script
5# Syncs music from local storage to Tailscale-connected Navidrome server
6# Optimized for low-power Atom processor
7################################################################################
8
9# === CONFIGURATION ===
10MUSIC_SOURCE="/your/music/directory"
11RCLONE_REMOTE="what_you_named_your_navidrome_server:/your/music/directory"
12FILTER_FILE="$HOME/.config/rclone/what_you_named_your_filter.txt"
13
14# Navidrome API settings
15NAVIDROME_URL="http://XXX.XX.XX.XX:4533"
16NAVIDROME_USER="YOUR USER NAME"
17NAVIDROME_PASS="YOUR PASSWORD"
18
19# === SCRIPT START ===
20echo "========================================"
21echo "Navidrome Local Server Music Sync"
22echo "Started: $(date)"
23echo "========================================"
24
25# Check if source directory exists
26if [ ! -d "$MUSIC_SOURCE" ]; then
27 echo "ERROR: Music source directory not found: $MUSIC_SOURCE"
28 exit 1
29fi
30
31# Check if filter file exists
32if [ ! -f "$FILTER_FILE" ]; then
33 echo "WARNING: Filter file not found: $FILTER_FILE"
34 echo "Proceeding without filters..."
35 FILTER_ARG=""
36else
37 FILTER_ARG="--filter-from $FILTER_FILE"
38fi
39
40# Perform rclone sync with optimized settings for local network
41echo ""
42echo "Starting music sync..."
43echo "Source: $MUSIC_SOURCE"
44echo "Destination: $RCLONE_REMOTE"
45echo ""
46
47rclone sync "$MUSIC_SOURCE" "$RCLONE_REMOTE" \
48 $FILTER_ARG \
49 --progress \
50 --stats 10s \
51 --transfers 8 \
52 --checkers 16 \
53 --stats-one-line \
54 --bwlimit-file 50M
55
56# Check sync result
57SYNC_EXIT_CODE=$?
58
59if [ $SYNC_EXIT_CODE -eq 0 ]; then
60 echo ""
61 echo "✓ Sync completed successfully!"
62 echo ""
63
64 # Trigger Navidrome library scan
65 echo "Triggering Navidrome library scan..."
66
67 # Generate salt and MD5 token for authentication
68 SALT=$(date +%s | md5sum | cut -d' ' -f1 | cut -c1-12)
69 TOKEN=$(echo -n "${NAVIDROME_PASS}${SALT}" | md5sum | cut -d' ' -f1)
70
71 # Call the startScan API
72 SCAN_RESPONSE=$(curl -s "${NAVIDROME_URL}/rest/startScan?u=${NAVIDROME_USER}&t=${TOKEN}&s=${SALT}&v=1.16.1&c=rclone_sync&f=json")
73
74 # Check response
75 if echo "$SCAN_RESPONSE" | grep -q '"status":"ok"'; then
76 if echo "$SCAN_RESPONSE" | grep -q '"scanning":true'; then
77 echo "✓ Library scan triggered successfully!"
78 echo " Navidrome is now scanning your music library..."
79 else
80 echo "✓ Scan request accepted"
81 fi
82 elif echo "$SCAN_RESPONSE" | grep -q '"status":"failed"'; then
83 ERROR_MSG=$(echo "$SCAN_RESPONSE" | grep -o '"message":"[^"]*"' | cut -d'"' -f4)
84 echo "⚠ Scan failed: $ERROR_MSG"
85 else
86 echo "⚠ Unexpected response"
87 echo "$SCAN_RESPONSE"
88 fi
89
90else
91 echo ""
92 echo "✗ Sync failed with exit code: $SYNC_EXIT_CODE"
93 echo "Skipping Navidrome scan."
94 exit 1
95fi
96
97echo ""
98echo "========================================"
99echo "Completed: $(date)"
100echo "========================================"Resources
- Navidrome Docs: https://www.navidrome.org/docs/
- Navidrome Config Options: https://www.navidrome.org/docs/usage/configuration-options/
- Tailscale Docs: https://tailscale.com/kb/
- rclone Docs: https://rclone.org/docs/