init
This commit is contained in:
commit
404deff6e4
|
@ -0,0 +1 @@
|
|||
3.13
|
|
@ -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"
|
|
@ -0,0 +1,7 @@
|
|||
asyncio
|
||||
shazamio
|
||||
click
|
||||
python_dotenv
|
||||
tqdm
|
||||
joblib
|
||||
mutagen
|
|
@ -0,0 +1,2 @@
|
|||
def main() -> None:
|
||||
print("Hello from music!")
|
|
@ -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()
|
|
@ -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()
|
|
@ -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 },
|
||||
]
|
Loading…
Reference in New Issue