Skip to content

Commit

Permalink
0.12
Browse files Browse the repository at this point in the history
Visual album art and --quiet mode
  • Loading branch information
lukafilipxvic committed Sep 30, 2024
1 parent fc72b4c commit be501bc
Show file tree
Hide file tree
Showing 10 changed files with 85 additions and 37 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Byte-compiled / optimized / DLL files
.env
__pycache__/
*.py[cod]
*$py.class
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

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

<p align="center">
<img src="https://github.com/lukafilipxvic/pyzam/blob/main/images/pyzam-usage.gif" alt="Pyzam usage", width"459">
</p>

## Installation

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

```bash
# Loop the recognition continously and save the logs as CSV file
pyzam --speaker -d 10 --loop
pyzam --speaker -d 10 --write --loop

# Listen to mixtapes and save the logs as CSV file
pyzam --input audio_file.mp3 --duration 12 --mixtape
Expand All @@ -66,8 +70,9 @@ See `pyzam --help` for more options.
| --microphone, -m | Listens to the microphone of your device.
| --speaker, -s | Listens to the speaker of your device (default).
| --url, -u | Detects from the given URL to an audio file.
| --help, -h | Show usage & options and exit.
| --help, -h | Show usage, options and exit.
| --duration, -d | Length of microphone or speaker recording. Max = 12 seconds.
| --quiet, -q | Supresses the operation messages (i.e. Recording speaker for X seconds...).
| --loop, -l | Loop the recognition process indefinitely.
| --mixtape | Detects every -d seconds for a given input file, only works with --input. --write is enabled automatically.
| --json, -j | Return the whole Shazamio output in JSON.
Expand Down
Binary file added images/pyzam-usage.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion pyzam/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.11"
__version__ = "0.12"
13 changes: 8 additions & 5 deletions pyzam/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#! /usr/bin/env python3

"""
Pyzam 0.11
Pyzam 0.12
A CLI music recognition tool for audio and mixtapes.
"""

Expand Down Expand Up @@ -47,6 +47,9 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument(
"-d", "--duration", type=int, default=5, help="audio recording duration (s)"
)
parser.add_argument(
"-q", "--quiet", action="store_true", help="suppress operation messages"
)
run_group.add_argument(
"--loop", "-l", action="store_true", help="loop music recognition process"
)
Expand Down Expand Up @@ -75,15 +78,15 @@ def check_ffmpeg():
def get_input_file(args, temp_dir) -> Path:
if args.microphone:
return record.microphone(
filename=f"{temp_dir}/pyzam_mic.wav", seconds=args.duration
filename=f"{temp_dir}/pyzam_mic.wav", seconds=args.duration, quiet=args.quiet
)
elif args.speaker:
return record.speaker(
filename=f"{temp_dir}/pyzam_speaker.wav", seconds=args.duration
filename=f"{temp_dir}/pyzam_speaker.wav", seconds=args.duration, quiet=args.quiet
)
elif args.url:
return record.url(url=args.url,
filename=f"{temp_dir}/pyzam_url.wav"
return record.url(
url=args.url, filename=f"{temp_dir}/pyzam_url.wav", quiet=args.quiet
)
else:
return args.input
Expand Down
Binary file added pyzam/data/default_album_cover.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 42 additions & 11 deletions pyzam/identify.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import asyncio
import climage
import csv
from datetime import datetime
import random
import requests
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
import pkg_resources
from shazamio import Shazam
import soundfile as sf
import tempfile
Expand All @@ -18,10 +20,8 @@ def write_csv(file_name: str, data_rows: list):
:param data_rows: Includes timestamp, track_title, artist and album_cover.
"""
header = ["Timestamp", "Track", "Artist", "Album Cover"]
csv_file = f"{file_name}.csv"
file_exists = os.path.isfile(csv_file)

with open(csv_file, mode="a", newline="", encoding="utf-8") as file:
file_exists = os.path.isfile(f"{file_name}.csv")
with open(f"{file_name}.csv", mode="a", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
if not file_exists:
writer.writerow(header)
Expand All @@ -36,17 +36,49 @@ def extract_track_info(out):
out["track"]
.get("images", {})
.get("coverart", "")
.replace("/400x400cc.jpg", "/1400x1400cc.png")
#.replace("/400x400cc.jpg", "/1400x1400cc.png")
)
else:
album_cover_hq = None
return track_title, artist, album_cover_hq


def print_track_info(track_info):
print(f"Track: {track_info[0]}")
print(f"Artist: {track_info[1]}")
print(f"Album Cover: {track_info[2]}")
track_name = f"Track: {track_info[0]}"
artist_name = f"Artist: {track_info[1]}"

default_cover_data = pkg_resources.resource_string(__name__, 'data/default_album_cover.png')
with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as temp_file:
temp_file.write(default_cover_data)
default_cover_path = temp_file.name

album_cover = climage.convert(default_cover_path, is_unicode=True, width=40)

if track_info[2]:
try:
response = requests.get(track_info[2], timeout=5)
response.raise_for_status()
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as temp_file:
temp_file.write(response.content)
temp_file_path = temp_file.name
album_cover = climage.convert(temp_file_path, is_unicode=True, width=40)
except (requests.RequestException, IOError) as e:
print(f"Error downloading album cover: {e}") # Provide feedback on error

cover_lines = album_cover.splitlines()
max_height = max(len(cover_lines), 2)

os.system('cls' if os.name == 'nt' else 'clear')
print() # Space on top
for i in range(max_height):
cover_line = cover_lines[i] if i < len(cover_lines) else ' ' * 25 # Padding for cover lines

if i == max_height - 2:
print(f"{cover_line} {track_name}")
elif i == max_height - 1:
print(f"{cover_line} {artist_name}")
else:
print(cover_line)


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

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

Expand Down Expand Up @@ -112,14 +145,12 @@ def split_and_identify(audio_file: str, duration: int):
task = progress.add_task(
f"[green]Splitting and identifying audio...", total=num_segments
)
# Make each segment an audio file to be used in Shazamio.
for i in range(num_segments):
start_idx = i * samples_per_duration
segment_data = data[start_idx : start_idx + samples_per_duration]
timestamp = datetime.utcfromtimestamp(start_idx / samplerate).strftime(
"%H:%M:%S"
)
# Temporarily saves audio file, shazams, then deletes.
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file:
sf.write(temp_file.name, segment_data, samplerate)
asyncio.run(
Expand Down
39 changes: 22 additions & 17 deletions pyzam/record.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,54 @@
import soundfile as sf
import io


def speaker(filename: str, seconds):
def speaker(filename: str, seconds, quiet=False):
"""
Records the device's speaker.
:param filename: Name and directory of the audio file written.
:param seconds: Duration to record (seconds).
:param quiet: If True, suppresses print statements.
"""
with sc.get_microphone(
id=str(sc.default_speaker().name), include_loopback=True
).recorder(samplerate=44100) as speaker:
print(f"Recording speaker for {seconds} seconds...")
speaker = sc.get_microphone(id=str(sc.default_speaker().name), include_loopback=True)
with speaker.recorder(samplerate=44100) as speaker_recorder:
if not quiet:
print()
print(f"Recording {speaker.name} for {seconds} seconds...")

data = speaker.record(numframes=44100 * seconds)
data = speaker_recorder.record(numframes=44100 * seconds)
sf.write(file=filename, data=data, samplerate=44100)
return filename


def microphone(filename: str, seconds):
def microphone(filename: str, seconds, quiet=False):
"""
Records the device's device.
Records the device's microphone.
:param filename: Name and directory of the audio file written.
:param seconds: Duration to record (seconds).
:param quiet: If True, suppresses recording statements.
"""
with sc.get_microphone(
id=str(sc.default_microphone().name), include_loopback=True
).recorder(samplerate=44100) as mic:
print(f"Recording microphone for {seconds} seconds...")
data = mic.record(numframes=44100 * seconds)
mic = sc.default_microphone()
with mic.recorder(samplerate=44100) as mic_recorder:
if not quiet:
print()
print(f"Recording {mic.name} for {seconds} seconds...")

data = mic_recorder.record(numframes=44100 * seconds)
sf.write(file=filename, data=data, samplerate=44100)
return filename


def url(url: str, filename: str):
def url(url: str, filename: str, quiet=False):
"""
Downloads audio from the provided URL.
:param url: URL of the audio file.
:param filename: Name and directory of the audio file written.
:param seconds: Duration to record (seconds).
:param quiet: If True, suppresses print statements.
"""
print(f"Downloading audio from URL...")
if not quiet:
print(f"Downloading audio from URL...")
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'}

try:
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
asyncio==3.4.3
climage==0.2.2
fastapi==0.111.0
pillow==10.4.0
requests==2.32.3
rich==13.8
soundfile==0.12.1
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setuptools.setup(
name="pyzam",
version="0.11",
version="0.12",
entry_points={"console_scripts": ["pyzam = pyzam.__main__:main"]},
author="lukafilipxvic",
description="A CLI music recognition tool for audio and mixtapes.",
Expand All @@ -20,6 +20,7 @@
"SoundCard",
],
packages=setuptools.find_packages(),
package_data={"pyzam": ["data/default_album_cover.png"]},
python_requires=">=3.9",
license="MIT",
classifiers=[
Expand Down

0 comments on commit be501bc

Please sign in to comment.