diff --git a/.gitignore b/.gitignore index a72afcf..f8e684f 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ /.cache/ /models/*.onnx *.wav -karaoke-generator-output-* +/karaoke-* # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/karaoke_generator/generator.py b/karaoke_generator/generator.py index 4ddcd5e..e2063ba 100644 --- a/karaoke_generator/generator.py +++ b/karaoke_generator/generator.py @@ -9,6 +9,7 @@ import subprocess import yt_dlp import slugify +import tldextract from audio_separator import Separator from lyrics_transcriber import LyricsTranscriber @@ -56,22 +57,15 @@ def __init__( self.output_dir = output_dir self.audio_file = None - self.youtube_url = None + self.source_url = None + self.source_site = None + self.source_video_id = None + self.input_source_slug = None - parsed_url = urllib.parse.urlparse(self.input_path) - if parsed_url.scheme and parsed_url.netloc: - self.youtube_url = self.input_path - self.input_source_slug = slugify.slugify(parsed_url.hostname + "-" + parsed_url.query, lowercase=False) - self.logger.debug(f"Input path was valid URL, set youtube_url and input_source_slug: {self.input_source_slug}") - elif os.path.exists(self.input_path): - self.audio_file = self.input_path - self.input_source_slug = slugify.slugify(os.path.basename(self.audio_file), lowercase=False) - self.logger.debug(f"Input path was valid file path, set audio_file and input_source_slug: {self.input_source_slug}") - else: - raise Exception("Input path must be either a valid file path or URL") + self.parse_input_source() if self.output_dir is None: - self.output_dir = os.path.join(os.getcwd(), "karaoke-generator-output-" + self.input_source_slug) + self.output_dir = os.path.join(os.getcwd(), "karaoke-" + self.input_source_slug) self.output_filename_slug = None self.youtube_video_file = None @@ -83,11 +77,36 @@ def __init__( self.output_values = {} self.create_folders() + def parse_input_source(self): + parsed_url = urllib.parse.urlparse(self.input_path) + if parsed_url.scheme and parsed_url.netloc: + self.source_url = self.input_path + + ext = tldextract.extract(parsed_url.netloc.lower()) + self.source_site = ext.registered_domain + + if "youtube" in self.source_site: + query = urllib.parse.parse_qs(parsed_url.query) + self.source_video_id = query["v"][0] + self.input_source_slug = "youtube-" + self.source_video_id + else: + self.input_source_slug = self.source_site + slugify.slugify("-" + parsed_url.path, lowercase=False) + + self.logger.debug(f"Input path was valid URL, set source_url and input_source_slug: {self.input_source_slug}") + elif os.path.exists(self.input_path): + self.audio_file = self.input_path + self.input_source_slug = slugify.slugify(os.path.basename(self.audio_file), lowercase=False) + self.logger.debug(f"Input path was valid file path, set audio_file and input_source_slug: {self.input_source_slug}") + else: + raise Exception("Input path must be either a valid file path or URL") + + self.input_source_slug = "-".join(filter(None, [slugify.slugify(self.artist), slugify.slugify(self.title), self.input_source_slug])) + def generate(self): self.logger.info("KaraokeGenerator beginning generation") - if self.audio_file is None and self.youtube_url is not None: - self.logger.debug(f"audio_file is none and youtube_url is {self.youtube_url}, fetching video from youtube") + if self.audio_file is None and self.source_url is not None: + self.logger.debug(f"audio_file is none and source_url is {self.source_url}, fetching video from youtube") self.download_youtube_video() self.separate_audio() @@ -188,7 +207,7 @@ def download_youtube_video(self): # Download the original highest quality file with yt_dlp.YoutubeDL(ydl_opts) as ydl: - youtube_info = ydl.extract_info(self.youtube_url, download=False) + youtube_info = ydl.extract_info(self.source_url, download=False) temp_download_filepath = ydl.prepare_filename(youtube_info) self.logger.debug(f"temp_download_filepath: {temp_download_filepath}") @@ -210,7 +229,7 @@ def download_youtube_video(self): with open(ydl_info_cache_file, "w") as cache_file: json.dump(ydl.sanitize_info(youtube_info), cache_file, indent=4) - ydl.download([self.youtube_url]) + ydl.download([self.source_url]) shutil.move(temp_download_filepath, youtube_info["download_filepath"]) self.youtube_video_file = youtube_info["download_filepath"] self.logger.debug(f"successfully downloaded youtube video to path: {self.youtube_video_file}") diff --git a/poetry.lock b/poetry.lock index 43779f3..b0fbc56 100644 --- a/poetry.lock +++ b/poetry.lock @@ -701,13 +701,13 @@ files = [ [[package]] name = "lyrics-transcriber" -version = "0.6.1" +version = "0.6.3" description = "Automatically create synchronised lyrics files in ASS and MidiCo LRC formats with word-level timestamps, using Whisper and lyrics from Genius and Spotify" optional = false python-versions = ">=3.9,<3.11" files = [ - {file = "lyrics_transcriber-0.6.1-py3-none-any.whl", hash = "sha256:2a080395805a5f7d54ff7d69366bf8a9a61bbeca3ab2a6dedb429885a0e23f57"}, - {file = "lyrics_transcriber-0.6.1.tar.gz", hash = "sha256:a659238b9015175146330d3bbd212697b62851af11e519e5f34a2039001df920"}, + {file = "lyrics_transcriber-0.6.3-py3-none-any.whl", hash = "sha256:4378260f1d7e29ac1da10f97eb890dfd5ed874fcbf5ddfda658f49c6318d77ac"}, + {file = "lyrics_transcriber-0.6.3.tar.gz", hash = "sha256:2270b1da6309267929380658b22039782873f7b1be630d8ae905c4d96537be9b"}, ] [package.dependencies] @@ -1349,6 +1349,21 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "requests-file" +version = "1.5.1" +description = "File transport adapter for Requests" +optional = false +python-versions = "*" +files = [ + {file = "requests-file-1.5.1.tar.gz", hash = "sha256:07d74208d3389d01c38ab89ef403af0cfec63957d53a0081d8eca738d0247d8e"}, + {file = "requests_file-1.5.1-py2.py3-none-any.whl", hash = "sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"}, +] + +[package.dependencies] +requests = ">=1.0.0" +six = "*" + [[package]] name = "resampy" version = "0.4.2" @@ -1639,6 +1654,23 @@ files = [ [package.extras] tests = ["flake8", "pytest", "pytest-cov"] +[[package]] +name = "tldextract" +version = "3.4.4" +description = "Accurately separates a URL's subdomain, domain, and public suffix, using the Public Suffix List (PSL). By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." +optional = false +python-versions = ">=3.7" +files = [ + {file = "tldextract-3.4.4-py3-none-any.whl", hash = "sha256:581e7dbefc90e7bb857bb6f768d25c811a3c5f0892ed56a9a2999ddb7b1b70c2"}, + {file = "tldextract-3.4.4.tar.gz", hash = "sha256:5fe3210c577463545191d45ad522d3d5e78d55218ce97215e82004dcae1e1234"}, +] + +[package.dependencies] +filelock = ">=3.0.8" +idna = "*" +requests = ">=2.1.0" +requests-file = ">=1.4" + [[package]] name = "tomli" version = "2.0.1" @@ -1883,4 +1915,4 @@ websockets = "*" [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "cb316b518c5b27cd6870a393d43bd16b8a48c537980bd2a0bce1e28e10bb80e4" +content-hash = "bcc6cb96cc382a168771a0cedf64d88fd460a638ae1f34c881193dafb255a7f0" diff --git a/pyproject.toml b/pyproject.toml index 20207d9..55daaab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "karaoke-generator" -version = "0.4.1" +version = "0.4.2" description = "Fully automated creation of _acceptable_ karaoke music videos from any music on YouTube, using open source tools and AI (e.g. Whisper and MDX-Net)" authors = ["Andrew Beveridge "] license = "MIT" @@ -12,9 +12,10 @@ python = ">=3.9,<3.11" yt-dlp = "^2023.6.22" pydub = "^0.25.1" audio-separator = "^0.3" -lyrics-transcriber = "^0.6" +lyrics-transcriber = "^0.6.3" python-slugify = "^8.0.1" regex = "^2023.6.3" +tldextract = "^3.4" # Note: after adding lyrics-transcriber with poetry lock, I then removed all traces of triton # from poetry.lock before running poetry install, as triton doesn't support macOS but isn't actually needed for whisper. # This was the only way I was able to get a working cross-platform build published to PyPI.