add buy.py; make scripts executable
Hunter
Tuesday, December 30th, 2025 at 4:41 PM
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)
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**