Skip to content

Commit be501bc

Browse files
committed
0.12
Visual album art and --quiet mode
1 parent fc72b4c commit be501bc

10 files changed

+85
-37
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Byte-compiled / optimized / DLL files
2+
.env
23
__pycache__/
34
*.py[cod]
45
*$py.class

README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515

1616
Pyzam is a free CLI music recognition tool for audio and mixtapes in Python.
1717

18+
<p align="center">
19+
<img src="https://github.com/lukafilipxvic/pyzam/blob/main/images/pyzam-usage.gif" alt="Pyzam usage", width"459">
20+
</p>
21+
1822
## Installation
1923

2024
### Dependencies
@@ -51,7 +55,7 @@ pyzam --url "https://archive.org/download/09-hold-me-in-your-arms/02%20-%20Never
5155

5256
```bash
5357
# Loop the recognition continously and save the logs as CSV file
54-
pyzam --speaker -d 10 --loop
58+
pyzam --speaker -d 10 --write --loop
5559

5660
# Listen to mixtapes and save the logs as CSV file
5761
pyzam --input audio_file.mp3 --duration 12 --mixtape
@@ -66,8 +70,9 @@ See `pyzam --help` for more options.
6670
| --microphone, -m | Listens to the microphone of your device.
6771
| --speaker, -s | Listens to the speaker of your device (default).
6872
| --url, -u | Detects from the given URL to an audio file.
69-
| --help, -h | Show usage & options and exit.
73+
| --help, -h | Show usage, options and exit.
7074
| --duration, -d | Length of microphone or speaker recording. Max = 12 seconds.
75+
| --quiet, -q | Supresses the operation messages (i.e. Recording speaker for X seconds...).
7176
| --loop, -l | Loop the recognition process indefinitely.
7277
| --mixtape | Detects every -d seconds for a given input file, only works with --input. --write is enabled automatically.
7378
| --json, -j | Return the whole Shazamio output in JSON.

images/pyzam-usage.gif

93.1 KB
Loading

pyzam/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.11"
1+
__version__ = "0.12"

pyzam/__main__.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#! /usr/bin/env python3
22

33
"""
4-
Pyzam 0.11
4+
Pyzam 0.12
55
A CLI music recognition tool for audio and mixtapes.
66
"""
77

@@ -47,6 +47,9 @@ def _parser() -> argparse.ArgumentParser:
4747
parser.add_argument(
4848
"-d", "--duration", type=int, default=5, help="audio recording duration (s)"
4949
)
50+
parser.add_argument(
51+
"-q", "--quiet", action="store_true", help="suppress operation messages"
52+
)
5053
run_group.add_argument(
5154
"--loop", "-l", action="store_true", help="loop music recognition process"
5255
)
@@ -75,15 +78,15 @@ def check_ffmpeg():
7578
def get_input_file(args, temp_dir) -> Path:
7679
if args.microphone:
7780
return record.microphone(
78-
filename=f"{temp_dir}/pyzam_mic.wav", seconds=args.duration
81+
filename=f"{temp_dir}/pyzam_mic.wav", seconds=args.duration, quiet=args.quiet
7982
)
8083
elif args.speaker:
8184
return record.speaker(
82-
filename=f"{temp_dir}/pyzam_speaker.wav", seconds=args.duration
85+
filename=f"{temp_dir}/pyzam_speaker.wav", seconds=args.duration, quiet=args.quiet
8386
)
8487
elif args.url:
85-
return record.url(url=args.url,
86-
filename=f"{temp_dir}/pyzam_url.wav"
88+
return record.url(
89+
url=args.url, filename=f"{temp_dir}/pyzam_url.wav", quiet=args.quiet
8790
)
8891
else:
8992
return args.input

pyzam/data/default_album_cover.png

1.48 KB
Loading

pyzam/identify.py

+42-11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import asyncio
2+
import climage
23
import csv
34
from datetime import datetime
4-
import random
5+
import requests
56
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
7+
import pkg_resources
68
from shazamio import Shazam
79
import soundfile as sf
810
import tempfile
@@ -18,10 +20,8 @@ def write_csv(file_name: str, data_rows: list):
1820
:param data_rows: Includes timestamp, track_title, artist and album_cover.
1921
"""
2022
header = ["Timestamp", "Track", "Artist", "Album Cover"]
21-
csv_file = f"{file_name}.csv"
22-
file_exists = os.path.isfile(csv_file)
23-
24-
with open(csv_file, mode="a", newline="", encoding="utf-8") as file:
23+
file_exists = os.path.isfile(f"{file_name}.csv")
24+
with open(f"{file_name}.csv", mode="a", newline="", encoding="utf-8") as file:
2525
writer = csv.writer(file)
2626
if not file_exists:
2727
writer.writerow(header)
@@ -36,17 +36,49 @@ def extract_track_info(out):
3636
out["track"]
3737
.get("images", {})
3838
.get("coverart", "")
39-
.replace("/400x400cc.jpg", "/1400x1400cc.png")
39+
#.replace("/400x400cc.jpg", "/1400x1400cc.png")
4040
)
4141
else:
4242
album_cover_hq = None
4343
return track_title, artist, album_cover_hq
4444

4545

4646
def print_track_info(track_info):
47-
print(f"Track: {track_info[0]}")
48-
print(f"Artist: {track_info[1]}")
49-
print(f"Album Cover: {track_info[2]}")
47+
track_name = f"Track: {track_info[0]}"
48+
artist_name = f"Artist: {track_info[1]}"
49+
50+
default_cover_data = pkg_resources.resource_string(__name__, 'data/default_album_cover.png')
51+
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
52+
temp_file.write(default_cover_data)
53+
default_cover_path = temp_file.name
54+
55+
album_cover = climage.convert(default_cover_path, is_unicode=True, width=40)
56+
57+
if track_info[2]:
58+
try:
59+
response = requests.get(track_info[2], timeout=5)
60+
response.raise_for_status()
61+
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
62+
temp_file.write(response.content)
63+
temp_file_path = temp_file.name
64+
album_cover = climage.convert(temp_file_path, is_unicode=True, width=40)
65+
except (requests.RequestException, IOError) as e:
66+
print(f"Error downloading album cover: {e}") # Provide feedback on error
67+
68+
cover_lines = album_cover.splitlines()
69+
max_height = max(len(cover_lines), 2)
70+
71+
os.system('cls' if os.name == 'nt' else 'clear')
72+
print() # Space on top
73+
for i in range(max_height):
74+
cover_line = cover_lines[i] if i < len(cover_lines) else ' ' * 25 # Padding for cover lines
75+
76+
if i == max_height - 2:
77+
print(f"{cover_line} {track_name}")
78+
elif i == max_height - 1:
79+
print(f"{cover_line} {artist_name}")
80+
else:
81+
print(cover_line)
5082

5183

5284
async def identify_audio(
@@ -72,6 +104,7 @@ async def identify_audio(
72104
out = await shazam.recognize(data=audio_file, proxy=None)
73105

74106
if "track" not in out:
107+
os.system('cls' if os.name == 'nt' else 'clear')
75108
print("No matches found.")
76109
return
77110

@@ -112,14 +145,12 @@ def split_and_identify(audio_file: str, duration: int):
112145
task = progress.add_task(
113146
f"[green]Splitting and identifying audio...", total=num_segments
114147
)
115-
# Make each segment an audio file to be used in Shazamio.
116148
for i in range(num_segments):
117149
start_idx = i * samples_per_duration
118150
segment_data = data[start_idx : start_idx + samples_per_duration]
119151
timestamp = datetime.utcfromtimestamp(start_idx / samplerate).strftime(
120152
"%H:%M:%S"
121153
)
122-
# Temporarily saves audio file, shazams, then deletes.
123154
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
124155
sf.write(temp_file.name, segment_data, samplerate)
125156
asyncio.run(

pyzam/record.py

+22-17
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,54 @@
33
import soundfile as sf
44
import io
55

6-
7-
def speaker(filename: str, seconds):
6+
def speaker(filename: str, seconds, quiet=False):
87
"""
98
Records the device's speaker.
109
1110
:param filename: Name and directory of the audio file written.
1211
:param seconds: Duration to record (seconds).
12+
:param quiet: If True, suppresses print statements.
1313
"""
14-
with sc.get_microphone(
15-
id=str(sc.default_speaker().name), include_loopback=True
16-
).recorder(samplerate=44100) as speaker:
17-
print(f"Recording speaker for {seconds} seconds...")
14+
speaker = sc.get_microphone(id=str(sc.default_speaker().name), include_loopback=True)
15+
with speaker.recorder(samplerate=44100) as speaker_recorder:
16+
if not quiet:
17+
print()
18+
print(f"Recording {speaker.name} for {seconds} seconds...")
1819

19-
data = speaker.record(numframes=44100 * seconds)
20+
data = speaker_recorder.record(numframes=44100 * seconds)
2021
sf.write(file=filename, data=data, samplerate=44100)
2122
return filename
2223

2324

24-
def microphone(filename: str, seconds):
25+
def microphone(filename: str, seconds, quiet=False):
2526
"""
26-
Records the device's device.
27+
Records the device's microphone.
2728
2829
:param filename: Name and directory of the audio file written.
2930
:param seconds: Duration to record (seconds).
31+
:param quiet: If True, suppresses recording statements.
3032
"""
31-
with sc.get_microphone(
32-
id=str(sc.default_microphone().name), include_loopback=True
33-
).recorder(samplerate=44100) as mic:
34-
print(f"Recording microphone for {seconds} seconds...")
35-
data = mic.record(numframes=44100 * seconds)
33+
mic = sc.default_microphone()
34+
with mic.recorder(samplerate=44100) as mic_recorder:
35+
if not quiet:
36+
print()
37+
print(f"Recording {mic.name} for {seconds} seconds...")
38+
39+
data = mic_recorder.record(numframes=44100 * seconds)
3640
sf.write(file=filename, data=data, samplerate=44100)
3741
return filename
3842

3943

40-
def url(url: str, filename: str):
44+
def url(url: str, filename: str, quiet=False):
4145
"""
4246
Downloads audio from the provided URL.
4347
4448
:param url: URL of the audio file.
4549
:param filename: Name and directory of the audio file written.
46-
:param seconds: Duration to record (seconds).
50+
:param quiet: If True, suppresses print statements.
4751
"""
48-
print(f"Downloading audio from URL...")
52+
if not quiet:
53+
print(f"Downloading audio from URL...")
4954
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246'}
5055

5156
try:

requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
asyncio==3.4.3
2+
climage==0.2.2
23
fastapi==0.111.0
4+
pillow==10.4.0
35
requests==2.32.3
46
rich==13.8
57
soundfile==0.12.1

setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="pyzam",
8-
version="0.11",
8+
version="0.12",
99
entry_points={"console_scripts": ["pyzam = pyzam.__main__:main"]},
1010
author="lukafilipxvic",
1111
description="A CLI music recognition tool for audio and mixtapes.",
@@ -20,6 +20,7 @@
2020
"SoundCard",
2121
],
2222
packages=setuptools.find_packages(),
23+
package_data={"pyzam": ["data/default_album_cover.png"]},
2324
python_requires=">=3.9",
2425
license="MIT",
2526
classifiers=[

0 commit comments

Comments
 (0)