commit 84e8d79412e3ef7cb45a0934be741c024af0f425
parent a8d26c6c8c9afbb710f8338f043bd6ca1e14918a
Author: Hunter
Date: Tue, 30 Dec 2025 17:48:48 -0500
support optional custom.css and custom.js; rename tracks/ to mix/
Diffstat:
9 files changed, 59 insertions(+), 48 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -5,10 +5,9 @@ __pycache__/
*.pyo
*.pyd
-# Tracks and playlists
-tracks/*
-!tracks/readme.md
-tracks/album_art.jpg
+# Mix folder (audio files, album art, custom CSS/JS)
+mix/*
+!mix/readme.md
# macOS
.DS_Store
diff --git a/generate_manifests.py b/generate_manifests.py
@@ -67,7 +67,7 @@ def get_configuration(localhost=False):
# File paths (no need to edit these)
SCRIPT_DIR = Path(__file__).parent.absolute()
-TRACKS_JSON = SCRIPT_DIR / "tracks" / "tracks.json"
+TRACKS_JSON = SCRIPT_DIR / "mix" / "tracks.json"
STYLES_CSS = SCRIPT_DIR / "resources" / "styles.css"
@@ -156,15 +156,15 @@ def generate_pwa_manifests(app_name=None, base_path=None):
"index.html",
"resources/styles.css",
"resources/script.js",
- "tracks/tracks.json",
+ "mix/tracks.json",
"resources/icon.png",
"resources/play.png",
"resources/pause.png",
"resources/prev.png",
"resources/next.png",
- "tracks/album_art.jpg"
+ "mix/album_art.jpg"
],
- "tracks": [f"tracks/{track['filename']}" for track in tracks]
+ "tracks": [f"mix/{track['filename']}" for track in tracks]
}
with open(SCRIPT_DIR / "resource-manifest.json", 'w', encoding='utf-8') as f:
@@ -185,8 +185,8 @@ const getBasePath = () => {{
const basePath = getBasePath();
-// Install event - cache only static resources (not MP3s)
-// MP3s will be cached by the main app's blob preloading system
+// Install event - cache only static resources (not audio files)
+// Audio files will be cached by the main app's blob preloading system
self.addEventListener('install', (event) => {{
console.log('Service Worker installing...', 'Base path:', basePath);
event.waitUntil(
diff --git a/index.html b/index.html
@@ -11,6 +11,7 @@
<link rel="manifest" href="manifest.json">
<title>my mixapp</title>
<link rel="stylesheet" href="resources/styles.css">
+ <link rel="stylesheet" href="mix/custom.css" onerror="this.remove()">
</head>
<body>
<div class="window-container">
@@ -39,5 +40,6 @@
<audio id="audioPlayer"></audio>
<script src="resources/script.js"></script>
+ <script src="mix/custom.js" onerror="this.remove()"></script>
</body>
</html>
diff --git a/mix/readme.md b/mix/readme.md
@@ -0,0 +1,10 @@
+# /mix
+
+add your audio files here, then run `scan.py` to create `tracks.json`.
+
+supported formats: `.mp3`, `.m4a`, `.ogg`, `.flac`, `.wav`
+
+## optional files
+- `album_art.jpg` - cover art for your mix
+- `custom.css` - custom styles to override the default theme
+- `custom.js` - custom scripts for additional functionality
diff --git a/readme.md b/readme.md
@@ -37,11 +37,12 @@ hits different, right?<br><br>
## quickstart
1. **prep your playlist**
- - add your .mp3 files to the `/tracks` directory, or use:
+ - add your audio files to the `/mix` directory, or use:
- `./rip.py` to rip tracks from a physical CD
- `./buy.py` to search for songs to purchase (uses iTunes on MacOS, <a href="https://song.link/i/1651294855">song.link</a> otherwise)
- - run `./scan.py` to parse `/tracks` and populate `tracks.json`, which defines the songs available to the player. after running `./scan.py` once, you can manually edit `tracks.json` to refine your mix.
- - optionally, add an `album_art.jpg` to `/tracks` to set the cover art for your mix.
+ - run `./scan.py` to parse `/mix` and populate `tracks.json`, which defines the songs available to the player. after running `./scan.py` once, you can manually edit `tracks.json` to refine your mix.
+ - optionally, add an `album_art.jpg` to `/mix` to set the cover art for your mix.
+ - supported audio formats: `.mp3`, `.m4a`, `.ogg`, `.flac`, `.wav`
2. **soundcheck**
- run `./host.py` to start a local HTTP server for testing. you can scan the QR code printed to the terminal to test the app from any device on your local network.
@@ -64,6 +65,9 @@ hits different, right?<br><br>
<img src="readme_images/lock_screen.jpeg" width="275"><br>
(pictured: integration with iOS lockscreen controls)
+## customization
+add `custom.css` and/or `custom.js` to the `/mix` folder to customize your mixapp's appearance and behavior. these files are loaded automatically if present.
+
## intellectual property notice
ensure you have the right to distribute any media files you include in public mixapps. personal archival backups are for your own use. sharing them with others, even as a gift, is not covered by fair use or backup exceptions.
diff --git a/resources/script.js b/resources/script.js
@@ -60,7 +60,7 @@ fetch('manifest.json')
}
// Now that we have CACHE_NAME, load tracks
- return fetch('tracks/tracks.json');
+ return fetch('mix/tracks.json');
})
.then(response => {
if (!response.ok) {
@@ -114,7 +114,7 @@ audio.addEventListener('play', () => {
if ('mediaSession' in navigator) {
// Convert relative path to absolute URL for media session
// Use document.baseURI to correctly resolve paths in subdirectories
- const albumArtUrl = new URL('tracks/album_art.jpg', document.baseURI).href;
+ const albumArtUrl = new URL('mix/album_art.jpg', document.baseURI).href;
navigator.mediaSession.metadata = new MediaMetadata({
title: song.title,
artist: song.artist,
@@ -275,7 +275,7 @@ function playSong(index) {
audio.src = blobUrl;
} else {
console.log(`Song not preloaded, loading: ${song.filename}`);
- audio.src = `tracks/${song.filename}`;
+ audio.src = `mix/${song.filename}`;
// Request priority preloading for this song
requestPriorityPreload(song.filename);
}
@@ -634,7 +634,7 @@ function applySeek(clickPercentage) {
if (preloadedAudio[song.filename]) {
audio.src = preloadedAudio[song.filename].blobUrl;
} else {
- audio.src = `tracks/${song.filename}`;
+ audio.src = `mix/${song.filename}`;
}
// Wait for metadata to be loaded before seeking
@@ -810,7 +810,7 @@ function preloadResources() {
'resources/pause.png',
'resources/prev.png',
'resources/next.png',
- 'tracks/album_art.jpg'
+ 'mix/album_art.jpg'
];
const imagePromises = resources.map(src => {
@@ -876,7 +876,7 @@ function preloadNextSong() {
function fetchAndPreloadSong(song, filename) {
// Use fetch to force full download of the entire file
- fetch(`tracks/${filename}`)
+ fetch(`mix/${filename}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -929,7 +929,7 @@ async function checkCachedTracks() {
// Check each song to see if it's cached
for (const song of songs) {
// Build the same absolute URL that storeBlobInCache uses for consistency
- const absoluteUrl = new URL(`tracks/${song.filename}`, window.location.href).href;
+ const absoluteUrl = new URL(`mix/${song.filename}`, window.location.href).href;
const isInCache = cachedRequests.some(request => request.url === absoluteUrl);
if (isInCache) {
cachedTracks.add(song.filename);
@@ -968,10 +968,10 @@ async function loadFromCache(filename) {
try {
const cache = await caches.open(CACHE_NAME);
// Try both relative and absolute URLs
- let response = await cache.match(`tracks/${filename}`);
+ let response = await cache.match(`mix/${filename}`);
if (!response) {
// Try with absolute URL
- const absoluteUrl = new URL(`tracks/${filename}`, window.location.href).href;
+ const absoluteUrl = new URL(`mix/${filename}`, window.location.href).href;
response = await cache.match(absoluteUrl);
}
@@ -1025,7 +1025,7 @@ async function storeBlobInCache(filename, blob) {
}
});
// Use absolute URL for consistency
- const absoluteUrl = new URL(`tracks/${filename}`, window.location.href).href;
+ const absoluteUrl = new URL(`mix/${filename}`, window.location.href).href;
await cache.put(absoluteUrl, response);
console.log(`✓ Cached for offline: ${filename}`);
} catch (error) {
@@ -1104,7 +1104,7 @@ function processPriorityPreload() {
function priorityFetchAndPreloadSong(song, filename) {
// Use fetch to force full download of the entire file
- fetch(`tracks/${filename}`)
+ fetch(`mix/${filename}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
diff --git a/rip.py b/rip.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
-Rips audio CDs to MP3 files in /tracks directory
+Rips audio CDs to MP3 files in /mix directory
Uses system tools: ffmpeg/ffprobe (no Python dependencies needed)
"""
@@ -13,7 +13,7 @@ from pathlib import Path
import platform
SCRIPT_DIR = Path(__file__).parent.absolute()
-TRACKS_DIR = SCRIPT_DIR / "tracks"
+MIX_DIR = SCRIPT_DIR / "mix"
def check_ffmpeg():
@@ -270,9 +270,9 @@ def rip_cd():
print("\n✓ ffmpeg found.")
# Create tracks directory if it doesn't exist
- if not TRACKS_DIR.exists():
- print(f"\nCreating {TRACKS_DIR.name} directory...")
- TRACKS_DIR.mkdir(parents=True, exist_ok=True)
+ if not MIX_DIR.exists():
+ print(f"\nCreating {MIX_DIR.name} directory...")
+ MIX_DIR.mkdir(parents=True, exist_ok=True)
# Find CD mount point
print("\nSearching for audio CD...")
@@ -299,7 +299,7 @@ def rip_cd():
# Confirm before ripping
print(f"\nThis will copy and convert {len(audio_files)} tracks to MP3 format.")
- print(f"Output directory: {TRACKS_DIR}")
+ print(f"Output directory: {MIX_DIR}")
# Prompt for artist name
print("\nEnter the artist name for this album.")
@@ -324,12 +324,12 @@ def rip_cd():
cleaned_name = re.sub(r'^\d+\s*[-.]?\s*', '', base_name) or base_name
padded_idx = str(idx).zfill(padding_width)
- output_file = TRACKS_DIR / f"{padded_idx} {cleaned_name}.mp3"
+ output_file = MIX_DIR / f"{padded_idx} {cleaned_name}.mp3"
# Handle duplicate filenames
counter = 1
while output_file.exists():
- output_file = TRACKS_DIR / f"{padded_idx} {cleaned_name}_{counter}.mp3"
+ output_file = MIX_DIR / f"{padded_idx} {cleaned_name}_{counter}.mp3"
counter += 1
print(f"[{idx}/{len(audio_files)}] {audio_file.name} -> {output_file.name}")
@@ -361,7 +361,7 @@ def rip_cd():
print(f"\n✓ Successfully ripped {success_count}/{len(audio_files)} tracks in {total_minutes}m {total_secs}s.")
if success_count > 0:
- print(f"\nTracks saved to: {TRACKS_DIR}")
+ print(f"\nTracks saved to: {MIX_DIR}")
print("\nNext steps:")
print(" 1. Run scan.py to generate tracks.json with metadata")
print(" 2. Run host.py to test your mixtape locally")
diff --git a/scan.py b/scan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
-Scans /tracks directory and populates tracks.json with metadata
+Scans /mix directory and populates tracks.json with metadata
Supports MP3, M4A, OGG, FLAC, and WAV formats
Automatically manages a virtual environment for dependencies
"""
@@ -13,8 +13,8 @@ from pathlib import Path
SCRIPT_DIR = Path(__file__).parent.absolute()
VENV_DIR = SCRIPT_DIR / "venv"
-TRACKS_DIR = SCRIPT_DIR / "tracks"
-OUTPUT_FILE = TRACKS_DIR / "tracks.json"
+MIX_DIR = SCRIPT_DIR / "mix"
+OUTPUT_FILE = MIX_DIR / "tracks.json"
REQUIREMENTS_FILE = SCRIPT_DIR / "requirements.txt"
@@ -73,11 +73,11 @@ def scan_tracks():
SUPPORTED_EXTENSIONS = ('.mp3', '.m4a', '.ogg', '.flac', '.wav')
# Check if tracks directory exists, create if it doesn't
- if not TRACKS_DIR.exists():
- print(f"Creating {TRACKS_DIR.name} directory...")
- TRACKS_DIR.mkdir(parents=True, exist_ok=True)
- print(f"✓ {TRACKS_DIR.name} directory created.")
- print(f"\nPlease add audio files to the {TRACKS_DIR.name} directory and run this script again.")
+ if not MIX_DIR.exists():
+ print(f"Creating {MIX_DIR.name} directory...")
+ MIX_DIR.mkdir(parents=True, exist_ok=True)
+ print(f"✓ {MIX_DIR.name} directory created.")
+ print(f"\nAdd audio files to the {MIX_DIR.name} directory and run this script again.")
print(f"Supported formats: {', '.join(SUPPORTED_EXTENSIONS)}")
sys.exit(0)
@@ -89,11 +89,11 @@ def scan_tracks():
sys.exit(0)
# Find all supported audio files
- audio_files = [f for f in TRACKS_DIR.iterdir() if f.suffix.lower() in SUPPORTED_EXTENSIONS]
+ audio_files = [f for f in MIX_DIR.iterdir() if f.suffix.lower() in SUPPORTED_EXTENSIONS]
if not audio_files:
- print(f"No audio files found in {TRACKS_DIR}")
- print(f"\nPlease add audio files to the {TRACKS_DIR.name} directory and run this script again.")
+ print(f"No audio files found in {MIX_DIR}")
+ print(f"\nPlease add audio files to the {MIX_DIR.name} directory and run this script again.")
print(f"Supported formats: {', '.join(SUPPORTED_EXTENSIONS)}")
sys.exit(0)
diff --git a/tracks/readme.md b/tracks/readme.md
@@ -1,3 +0,0 @@
-# /tracks
-
-add your .mp3 files here, then run `scan.py` to create `tracks.json`.
-\ No newline at end of file