Compare commits

...

4 Commits

Author SHA1 Message Date
6741fb447b add pyproject.toml 2024-05-02 23:34:38 +02:00
f5cbfffe82 2.1.0 2024-05-02 23:34:27 +02:00
c83f8c65e9 2.1.0 2024-05-02 23:27:28 +02:00
0845ababa3 move to argparse, split process_file 2024-05-01 23:45:48 +02:00
6 changed files with 142 additions and 160 deletions

View File

@ -1,6 +1,6 @@
# Maintainer: Matthias Quintern <matthiasqui@protonmail.com>
# Maintainer: Matthias Quintern <matthias(dot)quintern(at)posteo(dot)de>
pkgname=nicole
pkgver=2.0
pkgver=2.1.0
pkgrel=1
pkgdesc="Write lyrics from genius or azlyrics.com to a mp3-tag"
arch=('any')
@ -8,7 +8,9 @@ url="https:/github.com/MatthiasQuintern/nicole"
license=('GPL3')
depends=('python-mutagen' 'python-beautifulsoup4')
source=(file://${PWD}/nicole/nicole.py _nicole.compdef.zsh nicole.1.man)
md5sums=(93752797fc2bca526fbfd32611518065 f3b46bdcaba5e7fc23bbacc6b2e153c0 8c3b8e5c90afdc8993ebab78d48f5668)
md5sums=('1cfc6bca38b8e1b8c28694226eebb31e'
'a0a390f36de74a366065ab65bfd1d8de'
'8c3b8e5c90afdc8993ebab78d48f5668')
package() {
mkdir -p "${pkgdir}/usr/bin"

View File

@ -15,16 +15,16 @@ _nicole()
# option[description]:message:action
# # -s allow stacking, eg -inr
_arguments -s \
'-d[process directory]':directory:_directories \
'-f[process file]':file:_files \
'-r[go through directories recursively]' \
'-s[silent]' \
'-i[ignore history]' \
'-n[do not write to history]' \
'-o[overwrite if the file already has lyrics]' \
'-t[test, only print lyrics, dont write to tags]' \
'-h[show this]' \
'--rm_explicit[remove the "Explicit" lyrics warning from the title tag]' \
'--site[specify lyrics site]':lyrics-site:_lyrics-site
{--directory,-d}'[process directory]':directory:_directories \
{--file,-f}'[process file]':file:_files \
{--recursive,-r}'[go through directories recursively]' \
{--silent}'[silent]' \
{--ignore-history,-i}'[ignore history]' \
{--no-history,-n]}'[do not write to history]' \
{--overwrite,-o}'[overwrite if the file already has lyrics]' \
{--dry-run,-t}'[test, only print lyrics, dont write to tags]' \
'--help[show this]' \
'--rm-explicit[remove the "Explicit" lyrics warning from the title tag]' \
{--site,-s}'[specify lyrics site]':lyrics-site:_lyrics-site
}
_nicole "$@"

View File

@ -1,6 +1,6 @@
% NICOLE(1) nicole 2.0
% NICOLE(1) nicole 2.1.0
% Matthias Quintern
% April 2022
% May 2024
# NAME
**N**ew-**I**ntrepid-**C**hief-**O**f-**L**yrics-**E**mbedders (obviously)
@ -31,56 +31,52 @@ If you don't want your files in the history, add the `-n` option.
## genius
Nicole searches for lyrics using the genius api with the "title" and "artist" tags of the file.
If the title and artist names from genius and the tags are similar, the lyrics are scraped from the url obtained through the api.
If the title and artist names from genius are similar enough to the ones of the file,
the lyrics are scraped from the url obtained through the api.
## azlyrics
Nicole creates an azlyrics.com url from the "title" and "artist" tags of the file.
The lyrics are extracted from the html document using regex.
Unfortunately, there needs to be a 5 second delay between each request to azlyrics.com because the site will block your ip for a while if you send many requests.
Unfortunately, there needs to be a 5 second delay between each request to azlyrics.com because
the site will block your ip for a while if you send many requests.
## Important Note
Since the lyrics are extracted from html pages and not from an api, the lyrics sites might temporarily block your ip address if you send too many requests.
If that is the case, wait a few hours and try again.
# USAGE
## Command line options
**-d** directory
**--directory**, **-d** directory
: process directory [directory]
**-f** file
**--file**, **-f** file
: process file [file]
**-r**
**--recursive**, **-r**
: go through directories recursively
**-s**
**--silent**
: silent, no command-line output
**-i**
**--ignore-history**, **-i**
: ignore history
**-n**
**--no-history**, **-n**
: do not write to history
**-o**
**--overwrite**, **-o**
: overwrite if the file already has lyrics
**-t**
**--test**, **-t**
: test, do not write lyrics to file, but print to stdout
**-h**
: show this
**--rm_explicit**
**--rm-explicit**
: remove the "[Explicit]" lyrics warning from the song's title tag
**--site** site
**--site**, **-s** site
: onlysearch [site] for lyrics (genius or azlyrics)
If you do not specify a directory or file, the program will ask you if you want to use the current working directory.
Example: `nicole -ior -d ~/music/artist --rm_explicit`
One of `--file` and `--directory` must be given at least once.
Example: `nicole -ior -d ~/music/artist --rm-explicit`
# INSTALLATION AND UPDATING
To update nicole, simply follow the installation instructions.
@ -117,6 +113,12 @@ sudo chmod +x /usr/share/zsh/site-functions/_nicole
The dependencies will be automatically installed when using the either of the two installation options.
# CHANGELOG
## 2.1.0
- Refactoring:
- use argparse
- use pyproject.toml
- Ignore case when matching a genius result
## 2.0
- Nicole now supports lyrics from genius!
- Added man-page
@ -128,5 +130,5 @@ The dependencies will be automatically installed when using the either of the tw
- Files are now processed in order
# COPYRIGHT
Copyright © 2022 Matthias Quintern. License GPLv3+: GNU GPL version 3 <https://gnu.org/licenses/gpl.html>.\
Copyright © 2024 Matthias Quintern. License GPLv3+: GNU GPL version 3 <https://gnu.org/licenses/gpl.html>.\
This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.

View File

@ -1,5 +1,5 @@
#!/bin/python3
# Copyright © 2022 Matthias Quintern.
# Copyright © 2024 Matthias Quintern.
# This software comes with no warranty.
# This software is licensed under the GPL3
@ -10,11 +10,14 @@ import re
from bs4 import BeautifulSoup
from difflib import SequenceMatcher
from json import loads
import argparse
from os import path, getcwd, listdir, mkdir
from time import sleep
from sys import argv
version = "2.1.0"
# Der Name Nicole ist frei erfunden und hat keine Bedeutung.
# Jeglicher Zusammenhang mit einer Website der DHL wird hiermit ausdrücklich ausgeschlossen.
@ -31,9 +34,13 @@ class Nicole:
azlyrics:
Nicole creates a azlyrics url from the title and artist mp3-tags of the file.
The lyrics are extracted from the html document using regex.
genius:
Nicole searches the song from the title and artist mp3-tags via the genius api.
"""
allowed_extensions = [".mp3", ".flac"]
def __init__(self, test_run=False, silent=False, write_history=True, ignore_history=False, overwrite_tag=False, recursive=False, rm_explicit=False, lyrics_site="all"):
self.test_run = test_run
self.dry_run = test_run
self.silent = silent
self.write_history = write_history
@ -233,14 +240,14 @@ class Nicole:
genius_artist_featured = results[i]["result"]["artist_names"]
genius_title = results[i]["result"]["title"]
genius_title_featured = results[i]["result"]["title_with_featured"]
if SequenceMatcher(None, title, genius_title).ratio() < self.sanity_min_title_ratio:
if SequenceMatcher(None, title, genius_title_featured).ratio() < self.sanity_min_title_ratio:
if SequenceMatcher(None, title.lower(), genius_title.lower()).ratio() < self.sanity_min_title_ratio:
if SequenceMatcher(None, title.lower(), genius_title_featured.lower()).ratio() < self.sanity_min_title_ratio:
message += f"Genius result: titles do not match enough: '{title}' and '{genius_title}'/'{genius_title_featured}'\n "
i += 1
continue
if SequenceMatcher(None, artist, genius_artist).ratio() < self.sanity_min_artist_ratio:
if SequenceMatcher(None, artist, genius_artist_featured).ratio() < self.sanity_min_artist_ratio:
if SequenceMatcher(None, artist.lower(), genius_artist.lower()).ratio() < self.sanity_min_artist_ratio:
if SequenceMatcher(None, artist.lower(), genius_artist_featured.lower()).ratio() < self.sanity_min_artist_ratio:
message += f"Genius result: artists do not match enough: '{artist}' and '{genius_artist}'/'{genius_artist_featured}'\n "
i += 1
continue
@ -278,6 +285,10 @@ class Nicole:
return (True, lyrics)
def process_dir(self, directory):
f"""
Process all files from <directory> having a {Nicole.allowed_extensions} fileextension.
If recursive, call process_dir for subdirectories.
"""
if not path.isabs(directory):
directory = path.normpath(getcwd() + "/" + directory)
if not path.isdir(directory):
@ -296,30 +307,40 @@ class Nicole:
extension = path.splitext(entry)[1]
# if sound file with mp3 tags
if extension in [".mp3", ".flac"]:
success, message = self.process_file(entry)
# add to history
if self.write_history:
if entry not in self.history:
self.history.append(entry)
if not success:
self.failed.append(entry)
if not self.silent:
if success:
print(f"{entry}")
else:
print(f"{entry}")
print(" " + message)
print("History\n", self.history)
print("Failed\n", self.failed)
if extension in Nicole.allowed_extensions:
self.process_file(entry)
elif path.isdir(entry) and self.recursive:
self.process_dir(entry)
def process_file(self, file):
"""
process a file, append to history and print a message (depending on settings)
"""
success, message = self._process_file(file)
# add to history
if self.write_history:
if file not in self.history:
self.history.append(file)
if not success:
self.failed.append(file)
if not self.silent:
if success:
print(f"{file}")
else:
print(f"{file}")
print(" " + message)
# print("History\n", self.history)
# print("Failed\n", self.failed)
def _process_file(self, file):
"""
search for tags, write them to the file (if not dry run) and return wether successful or not and a message
"""
if not path.isabs(file):
file = path.normpath(getcwd() + "/" + file)
if not path.isfile(file):
@ -407,7 +428,7 @@ class Nicole:
message += lyrics
# if found lyrics
if success:
if self.test_run:
if self.dry_run:
print(f"\n\n{artist} - {title}:\n{lyrics}\n")
# write to tags
else:
@ -418,7 +439,7 @@ class Nicole:
audio["LYRICS"] = lyrics
audio.save()
else:
return (False, f"Could find lyrics but failed to write the tag.")
return (False, f"Could find lyrics but failed to write the tag (unknown audio type: {type(audio)})")
message += f"Written lyrics from {site} to {artist} - {title}"
return (True, message)
@ -427,106 +448,40 @@ class Nicole:
def main():
print("Nicole version 2.0")
print(f"Nicole version {version}")
helpstring = """Command line options:
-d [directory] process directory [directory]
-f [file] process file [file]
-r go through directories recursively
-s silent, no command-line output
-i ignore history
-n do not write to history
-o overwrite if the file already has lyrics
-t test, do not write lyrics to file, but print to console
-h show this
--rm_explicit remove the "[Explicit]" lyrics warning from the songs title tag
--site [site] use only [site]: azlyrics or genius
Visit https://github.com/MatthiasQuintern/nicole for updates and further help."""
args = []
if len(argv) > 1:
# iterate over argv list and extract the args
i = 1
while i < len(argv):
arg = argv[i]
if arg[0] == "-":
# check if option with arg, if yes add tuple to args
if len(argv) > i + 1 and argv[i+1][0] != "-":
args.append((arg.replace("-", ""), argv[i+1]))
i += 1
elif not "--" in arg:
for char in arg.replace("-", ""):
args.append(char)
else:
args.append(arg.replace("-", ""))
else:
print(f"Invalid or missing argument: '{arg}'")
print(helpstring)
return 1
parser = argparse.ArgumentParser(prog="nicole", description="lyrics scraper and embedder", epilog="https://github.com/MatthiasQuinter/nicole")
parser.add_argument("--directory", "-d", action="append", help="process directory [directory]")
parser.add_argument("--file", "-f", action="append", help="process file [file]")
parser.add_argument("--recursive", "-r", action="store_true", help="go through directories recursively")
parser.add_argument("--silent", action="store_true", help="silent, no command-line output")
parser.add_argument("--ignore-history", "-i", action="store_true", help="ignore history")
parser.add_argument("--no-history", "-n", action="store_true", help="do not write to history")
parser.add_argument("--overwrite", "-o", action="store_true", help="overwrite if the file already has lyrics")
parser.add_argument("--dry-run", "-t", action="store_true", help="test, do not write lyrics to file, but print to console")
parser.add_argument("--rm-explicit", action="store_true", help="remove the \"[Explicit]\" lyrics warning from the songs title tag")
parser.add_argument("--site", "-s", action="store", help="use only [site]: azlyrics or genius", default="all")
args = parser.parse_args()
i += 1
options = {
"t": False,
"s": False,
"n": True,
"i": False,
"o": False,
"r": False,
"h": False,
"rm_explicit": False,
}
directory = None
file = None
site = "all"
for arg in args:
if type(arg) == tuple:
if arg[0] == "d": directory = arg[1]
elif arg[0] == "f": file = arg[1]
elif arg[0] == "site":
if arg[1] in ["genius", "azlyrics", "all"]: site = arg[1]
else:
print(f"Invalid site: '{arg[1]}'")
elif arg in options.keys():
# flip the bool associated with the char
if options[arg] == False: options[arg] = True
else: options[arg] = False
else:
print(f"Invalid argument: '{arg}'")
print(helpstring)
return 1
# show help
if options["h"]:
print(helpstring)
return 0
if args.file is None and args.directory is None:
parser.error("Either --directory or --file is required")
# create nicole instance
nicole = Nicole(test_run=options["t"], silent=options["s"], write_history=options["n"], ignore_history=options["i"], overwrite_tag=options["o"], recursive=options["r"], rm_explicit=options["rm_explicit"], lyrics_site=site)
nicole = Nicole(test_run=args.dry_run, silent=args.silent, write_history=not args.no_history, ignore_history=args.ignore_history, overwrite_tag=args.overwrite,
recursive=args.recursive, rm_explicit=args.rm_explicit, lyrics_site=args.site)
# start with file or directory
if file:
success, message = nicole.process_file(file)
if not nicole.silent:
if success:
print(f"{file}")
else:
print(f"{file}")
print(" " + message)
elif directory:
try:
nicole.process_dir(directory)
except KeyboardInterrupt:
print("")
else:
use_wdir = input("No file or directory given. Use working directory? (y/n): ")
if use_wdir in "yY":
nicole.process_dir(getcwd())
else:
print(helpstring)
if type(args.file) == list:
for file in args.file:
try:
nicole.process_file(file)
except KeyboardInterrupt:
pass
if type(args.directory) == list:
for directory in args.directory:
try:
nicole.process_dir(directory)
except KeyboardInterrupt:
pass
if __name__ == "__main__":

23
pyproject.toml Normal file
View File

@ -0,0 +1,23 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
name = "nicole"
version = "2.1.0"
description = "Write lyrics from genius or azlyrics.com into a mp3-tag"
author = "Matthias Quintern"
author_email = "matthias.quintern@posteo.de"
url = "https://github.com/MatthiasQuintern/nicole.git"
license = "GPLv3"
packages = ["nicole"]
install_requires = ["mutagen", "beautifulsoup4"]
[tool.setuptools.entry-points]
console_scripts = ["nicole=nicole.nicole:main"]
[tool.setuptools.metadata]
description-file = "readme.md"
[tool.setuptools.packages.find]
where = "."

View File

@ -2,7 +2,7 @@ from setuptools import setup
setup(
name="nicole",
version="2.0",
version="2.1.0",
description="Write lyrics from genius or azlyrics.com to a mp3-tag",
author="Matthias Quintern",