This commit is contained in:
publicmatt 2025-01-20 12:00:18 -08:00
commit 404deff6e4
8 changed files with 278 additions and 0 deletions

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.13

0
README.md Normal file
View File

19
pyproject.toml Normal file
View File

@ -0,0 +1,19 @@
[project]
name = "music"
version = "0.1.0"
description = "music utilies"
readme = "README.md"
authors = [{ name = "publicmatt", email = "git@publicmatt.com" }]
requires-python = ">=3.13"
dependencies = [
"click>=8.1.8",
"joblib>=1.4.2",
"python-dotenv>=1.0.1",
]
[project.scripts]
music = "music.__main__:cli"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
asyncio
shazamio
click
python_dotenv
tqdm
joblib
mutagen

2
src/music/__init__.py Normal file
View File

@ -0,0 +1,2 @@
def main() -> None:
print("Hello from music!")

54
src/music/__main__.py Normal file
View File

@ -0,0 +1,54 @@
from enum import Enum
import click
import subprocess
# from .shazam import rename
from click.exceptions import ClickException
@click.group()
def cli():
pass
class Stream(str, Enum):
KEXP = "kexp"
KUGS = "kugs"
CLIS = "clis"
@cli.command("stream")
@click.option(
"-s", "--stream", "stream", type=click.Choice(Stream), default=Stream.KEXP
)
def play_stream(stream):
"""Play the KEXP stream using mpv."""
match stream:
case Stream.KEXP:
url = "https://kexp-mp3-128.streamguys1.com/kexp128.mp3"
case Stream.KUGS:
url = "https://peridot.streamguys1.com:7175/kugs-mp3""
case Stream.CLIS:
url = "https://stream2.statsradio.com:8012/stream?=&&___cb=759135934160766"
case _:
raise ClickException(f"unrecognized stream: {stream}")
subprocess.run(["mpv", url])
@cli.command("random")
def play_random():
"""todo."""
raise NotImplementedError("todo")
# fd -t file -e mp3 -p -a "fall|winter|spring|summer" $HOME/Music/ | mpv --playlist=- --shuffle --no-video
@cli.command("save")
@click.option( "-u", "--url", "url", required=True)
def save_song(url):
"""todo."""
raise NotImplementedError("todo")
# FILENAME=$(yt-dlp {{url}} -x --audio-format mp3 | tee /dev/tty | grep 'ExtractAudio' | cut -d ' ' -f 3-)
# $HOME/.local/lib/shazam/bin/shazam.py rename --song "$FILENAME"
if __name__ == "__main__":
cli()

137
src/music/shazam/shazam.py Executable file
View File

@ -0,0 +1,137 @@
#! /home/user/.venv/bin/python
import asyncio
from shazamio import Shazam, Serialize
from pathlib import Path
import click
from dotenv import load_dotenv
import os
from tqdm import tqdm
from typing import Optional
import sys
from collections import namedtuple
from joblib import Memory
from pydantic import BaseModel, Field
base_path = Path(__file__).parent.parent
memory = Memory(base_path / "./.cache", verbose=0)
class Match(BaseModel):
old: Path
new: Optional[Path] = Field(default=None)
title: Optional[str] = Field(default=None)
artist: Optional[str] = Field(default=None)
@click.group()
def cli():
pass
@cli.command()
@click.option(
"-s",
"--song",
required=True,
type=click.Path(exists=True, dir_okay=False, path_type=Path),
)
def rename(song: Path):
change = get_match(song)
if change and change.new is not None:
prompt_rename(change)
def songs(root: Path):
seasons = ["winter", "spring", "summer", "fall"]
pattern = "{}_*/*.mp3"
matches = []
for season in seasons:
for file_path in root.rglob(pattern.format(season)):
matches.append(file_path)
def small(f: Path, max_bytes: int = 10_000_000):
return f.stat().st_size < max_bytes
return filter(small, matches)
def needs_rename(song: Match):
if song.new is None:
print(f"\nno match: {song.old.name}\n")
return False
if song.old.name == song.new.name:
print(f"\nname correct: {song.old.name}\n")
return False
return True
@cli.command("main")
def main():
root = Path(os.getenv("MUSIC_PATH", "~/Music"))
if not root.exists():
raise ValueError(f"{str(root)} does not exist")
renames = []
for song in tqdm(songs(root), position=0, leave=True):
rename = get_match(song)
if rename and rename.new is not None:
renames.append(rename)
for song in filter(needs_rename, renames):
prompt_rename(song)
sys.exit(127)
def prompt_rename(song: Match):
old = song.old
answer = input(
f'\nrename: "{old.name}"\nto: "{song.new.name}"?\n[y[es]/n[o]/m[anual]] > '
)
if answer in ["n", "N"]:
print(f"\nskipping: {old.name}\n")
return
if answer in ["m"]:
manual = input("\nnew name: ")
new = old.parent / manual
assert (
new.suffix == old.suffix
), f"filetype changed: {old.suffix} -> {new.suffix}"
else:
new = song.new
try:
old.rename(new)
from mutagen.easyid3 import EasyID3
tags = EasyID3(new)
tags["title"] = song.title
tags["artist"] = song.artist
tags.save()
except FileNotFoundError as e:
print(f"error renaming: {e}")
print(f'old: "{old}"\nnew: "{new}"')
return
@memory.cache
def get_match(original: Path) -> Match:
loop = asyncio.get_event_loop()
shazam = Shazam()
try:
out = loop.run_until_complete(shazam.recognize(str(original)))
serialized = Serialize.track(out["track"])
rename = f"{serialized.title} - {serialized.subtitle}{original.suffix}"
new = Path(original.parent) / rename
return Match(
old=original, new=new, title=serialized.title, artist=serialized.subtitle
)
except:
return Match(old=original, new=None)
if __name__ == "__main__":
env = Path(__file__).parent.parent / ".env"
if not load_dotenv(env):
raise ValueError(".env not found: {env}")
cli()

58
uv.lock Normal file
View File

@ -0,0 +1,58 @@
version = 1
requires-python = ">=3.13"
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "joblib"
version = "1.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 },
]
[[package]]
name = "music"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "joblib" },
{ name = "python-dotenv" },
]
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.1.8" },
{ name = "joblib", specifier = ">=1.4.2" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]