add buy.py; make scripts executable

Hunter
Tuesday, December 30th, 2025 at 4:41 PM

buy.pynew
generate_manifests.py  
host.py  
readme.md+6-5
scan.py  

buy.py
  1 +#!/usr/bin/env python3
  2 +"""
  3 +Search for songs and open purchase links.
  4 +
  5 +Usage:
  6 +	./buy.py <search query>
  7 +	./buy.py beatles yesterday
  8 +	./buy.py "daft punk revolution 909"
  9 +"""
 10 +
 11 +import sys
 12 +import urllib.parse
 13 +import urllib.request
 14 +import json
 15 +import platform
 16 +import webbrowser
 17 +
 18 +
 19 +def search_itunes(query):
 20 +	"""Search iTunes for a song and return results."""
 21 +	encoded_query = urllib.parse.quote(query)
 22 +	url = f"https://itunes.apple.com/search?term={encoded_query}&media=music&entity=song&limit=10"
 23 +	
 24 +	try:
 25 +		with urllib.request.urlopen(url) as response:
 26 +			data = json.loads(response.read().decode())
 27 +			return data.get('results', [])
 28 +	except Exception as e:
 29 +		print(f"Error searching iTunes: {e}")
 30 +		return []
 31 +
 32 +
 33 +def open_itunes_link(itunes_url, track_id):
 34 +	"""Open the iTunes purchase link directly in iTunes app on macOS, or song.link on other platforms."""
 35 +	import subprocess
 36 +	
 37 +	# Convert to geo-aware link and add app=itunes parameter to force iTunes Store
 38 +	# This prevents opening in Apple Music (streaming) instead of iTunes Store (purchase)
 39 +	
 40 +	# Replace music.apple.com with geo.itunes.apple.com for better compatibility
 41 +	if 'music.apple.com' in itunes_url:
 42 +		itunes_url = itunes_url.replace('music.apple.com', 'geo.itunes.apple.com')
 43 +	elif 'itunes.apple.com' in itunes_url and 'geo.' not in itunes_url:
 44 +		itunes_url = itunes_url.replace('itunes.apple.com', 'geo.itunes.apple.com')
 45 +	
 46 +	# Add app=itunes parameter to force iTunes Store (not Apple Music)
 47 +	if '?' in itunes_url:
 48 +		# URL already has parameters, append with &
 49 +		if 'app=' not in itunes_url:
 50 +			itunes_url += '&app=itunes'
 51 +	else:
 52 +		# No parameters yet, add with ?
 53 +		itunes_url += '?app=itunes'
 54 +	
 55 +	# Convert https:// to itms:// to open directly in iTunes app
 56 +	itunes_url = itunes_url.replace('https://', 'itms://')
 57 +	
 58 +	# Use macOS 'open' command to bypass browser entirely
 59 +	if platform.system() == 'Darwin':  # macOS
 60 +		subprocess.run(['open', itunes_url])
 61 +		return itunes_url
 62 +	else:
 63 +		# For non-macOS systems, open song.link page with all platform options
 64 +		songlink_url = f"https://song.link/i/{track_id}"
 65 +		webbrowser.open(songlink_url)
 66 +		return songlink_url
 67 +
 68 +
 69 +def main():
 70 +	"""Main function to run the music search CLI."""
 71 +	# Get search query from command line args or prompt user
 72 +	if len(sys.argv) > 1:
 73 +		query = ' '.join(sys.argv[1:])
 74 +	else:
 75 +		query = input("Enter song name, artist, or both: ").strip()
 76 +	
 77 +	if not query:
 78 +		print("No search query provided.")
 79 +		return
 80 +	
 81 +	print(f"Searching for: {query}")
 82 +	
 83 +	# Search iTunes
 84 +	results = search_itunes(query)
 85 +	
 86 +	if not results:
 87 +		print("No results found. Try a different search term.")
 88 +		return
 89 +	
 90 +	# Get the best (first) result
 91 +	best_track = results[0]
 92 +	
 93 +	# Extract track info
 94 +	artist = best_track.get('artistName', 'Unknown')
 95 +	song = best_track.get('trackName', 'Unknown')
 96 +	itunes_url = best_track.get('trackViewUrl')
 97 +	track_id = best_track.get('trackId')
 98 +	
 99 +	if not itunes_url or not track_id:
100 +		print("Error: Could not find iTunes URL")
101 +		return
102 +	
103 +	# Open iTunes purchase page (macOS) or song.link (other platforms)
104 +	print(f"Opening: {artist} - {song}")
105 +	if platform.system() == 'Darwin':
106 +		print("(Opening iTunes Store directly)")
107 +	else:
108 +		print("(Opening song.link with all platform options)")
109 +	
110 +	open_itunes_link(itunes_url, track_id)
111 +
112 +
113 +if __name__ == "__main__":
114 +	try:
115 +		main()
116 +	except KeyboardInterrupt:
117 +		print("\n\nExiting...")
118 +		sys.exit(0)
generate_manifests.py
host.py
readme.md
37  
38  ## quickstart
39  1. **prep your playlist**
   -	- add your .mp3 files to the `/tracks` directory
   -		- you can do this manually or run `rip.py` to rip tracks from a physical CD.
   -	- 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.
40 +	- add your .mp3 files to the `/tracks` directory, or use:
41 +		- `./rip.py` to rip tracks from a physical CD
42 +		- `./buy.py` to search for songs to purchase (uses iTunes on MacOS, <a href="https://song.link/i/1651294855">song.link</a> otherwise)
43 +	- 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.
44  	- optionally, add  an `album_art.jpg` to `/tracks` to set the cover art for your mix.
45  
46  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.
47 +	- 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.
48  
49  3. **manifesting**
   -	- run `generate_manifests.py` and follow the interactive prompts to specify an app name and the remote server path where your app will be hosted.
50 +	- run `./generate_manifests.py` and follow the interactive prompts to specify an app name and the remote server path where your app will be hosted.
51  		- this creates the config files that enable offline functionality: `manifest.json`, `resource-manifest.json`, and `service-worker.js`.
52  
53  4. **ship it**
scan.py