music-kraken-core/music_kraken/cli/main_downloader.py

461 lines
14 KiB
Python
Raw Normal View History

import random
2023-06-12 12:56:14 +00:00
from typing import Set, Type, Dict, List
from pathlib import Path
import re
2023-06-20 14:40:34 +00:00
from .utils import cli_function
from .options.first_config import initial_config
2023-06-20 14:40:34 +00:00
2024-05-07 11:34:18 +00:00
from ..utils import output, BColors
2023-09-10 14:54:06 +00:00
from ..utils.config import write_config, main_settings
2024-04-09 12:00:51 +00:00
from ..utils.shared import URL_PATTERN
2023-06-20 14:40:34 +00:00
from ..utils.string_processing import fit_to_file_system
2023-10-23 14:21:44 +00:00
from ..utils.support_classes.query import Query
from ..utils.support_classes.download_result import DownloadResult
2024-05-07 11:34:18 +00:00
from ..utils.exception import MKInvalidInputException
from ..utils.exception.download import UrlNotFoundException
2024-01-16 08:53:51 +00:00
from ..utils.enums.colors import BColors
from .. import console
2024-05-07 11:34:18 +00:00
from ..download.results import Results, Option, PageResults, GoToResults
2023-06-20 14:40:34 +00:00
from ..download.page_attributes import Pages
from ..pages import Page
from ..objects import Song, Album, Artist, DatabaseObject
2023-06-12 12:56:14 +00:00
"""
This is the implementation of the Shell
# Behaviour
## Searching
```mkshell
> s: {querry or url}
# examples
> s: https://musify.club/release/some-random-release-183028492
> s: r: #a an Artist #r some random Release
```
Searches for an url, or an query
### Query Syntax
```
#a {artist} #r {release} #t {track}
```
You can escape stuff like `#` doing this: `\#`
## Downloading
To download something, you either need a direct link, or you need to have already searched for options
```mkshell
> d: {option ids or direct url}
# examples
> d: 0, 3, 4
> d: 1
> d: https://musify.club/release/some-random-release-183028492
```
## Misc
### Exit
```mkshell
> q
> quit
> exit
> abort
```
### Current Options
```mkshell
> .
```
### Previous Options
```
> ..
```
"""
EXIT_COMMANDS = {"q", "quit", "exit", "abort"}
ALPHABET = "abcdefghijklmnopqrstuvwxyz"
PAGE_NAME_FILL = "-"
MAX_PAGE_LEN = 21
def get_existing_genre() -> List[str]:
"""
gets the name of all subdirectories of shared.MUSIC_DIR,
but filters out all directories, where the name matches with any patern
from shared.NOT_A_GENRE_REGEX.
"""
existing_genres: List[str] = []
# get all subdirectories of MUSIC_DIR, not the files in the dir.
2023-09-10 14:54:06 +00:00
existing_subdirectories: List[Path] = [f for f in main_settings["music_directory"].iterdir() if f.is_dir()]
2023-06-12 12:56:14 +00:00
for subdirectory in existing_subdirectories:
name: str = subdirectory.name
2023-09-10 14:54:06 +00:00
if not any(re.match(regex_pattern, name) for regex_pattern in main_settings["not_a_genre_regex"]):
2023-06-12 12:56:14 +00:00
existing_genres.append(name)
existing_genres.sort()
return existing_genres
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
def get_genre():
existing_genres = get_existing_genre()
for i, genre_option in enumerate(existing_genres):
print(f"{i + 1:0>2}: {genre_option}")
while True:
genre = input("Id or new genre: ")
if genre.isdigit():
genre_id = int(genre) - 1
if genre_id >= len(existing_genres):
print(f"No genre under the id {genre_id + 1}.")
continue
return existing_genres[genre_id]
new_genre = fit_to_file_system(genre)
agree_inputs = {"y", "yes", "ok"}
verification = input(f"create new genre \"{new_genre}\"? (Y/N): ").lower()
if verification in agree_inputs:
return new_genre
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
def help_message():
print()
print(random.choice(main_settings["happy_messages"]))
2023-06-12 12:56:14 +00:00
print()
2023-06-20 14:40:34 +00:00
class Downloader:
2023-06-12 12:56:14 +00:00
def __init__(
self,
2024-01-15 11:48:36 +00:00
exclude_pages: Set[Type[Page]] = None,
2023-06-12 12:56:14 +00:00
exclude_shady: bool = False,
max_displayed_options: int = 10,
option_digits: int = 3,
2023-06-20 17:30:48 +00:00
genre: str = None,
process_metadata_anyway: bool = False,
2023-06-12 12:56:14 +00:00
) -> None:
self.pages: Pages = Pages(exclude_pages=exclude_pages, exclude_shady=exclude_shady)
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
self.page_dict: Dict[str, Type[Page]] = dict()
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
self.max_displayed_options = max_displayed_options
self.option_digits: int = option_digits
2024-01-15 11:48:36 +00:00
self.current_results: Results = None
self._result_history: List[Results] = []
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
self.genre = genre or get_genre()
2023-06-20 17:30:48 +00:00
self.process_metadata_anyway = process_metadata_anyway
2024-01-15 11:48:36 +00:00
2024-05-10 15:33:07 +00:00
output()
output(f"Downloading to: \"{self.genre}\"", color=BColors.HEADER)
output()
2023-06-12 12:56:14 +00:00
def print_current_options(self):
self.page_dict = dict()
2023-06-12 17:46:46 +00:00
print()
2023-06-12 12:56:14 +00:00
page_count = 0
2024-05-07 11:34:18 +00:00
for option in self.current_results.formatted_generator():
2023-06-12 12:56:14 +00:00
if isinstance(option, Option):
r = f"{BColors.GREY.value}{option.index:0{self.option_digits}}{BColors.ENDC.value} {option.music_object.option_string}"
print(r)
2023-06-12 12:56:14 +00:00
else:
2024-01-15 11:48:36 +00:00
prefix = ALPHABET[page_count % len(ALPHABET)]
print(
f"{BColors.HEADER.value}({prefix}) --------------------------------{option.__name__:{PAGE_NAME_FILL}<{MAX_PAGE_LEN}}--------------------{BColors.ENDC.value}")
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
self.page_dict[prefix] = option
self.page_dict[option.__name__] = option
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
page_count += 1
2023-06-12 17:46:46 +00:00
print()
2023-06-12 12:56:14 +00:00
def set_current_options(self, current_options: Results):
2023-09-10 14:54:06 +00:00
if main_settings["result_history"]:
self._result_history.append(current_options)
2024-01-15 11:48:36 +00:00
2023-09-10 14:54:06 +00:00
if main_settings["history_length"] != -1:
if len(self._result_history) > main_settings["history_length"]:
self._result_history.pop(0)
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
self.current_results = current_options
2024-01-15 11:48:36 +00:00
def previous_option(self) -> bool:
2023-09-10 14:54:06 +00:00
if not main_settings["result_history"]:
print("History is turned of.\nGo to main_settings, and change the value at 'result_history' to 'true'.")
return False
2024-01-15 11:48:36 +00:00
if len(self._result_history) <= 1:
print(f"No results in history.")
return False
self._result_history.pop()
self.current_results = self._result_history[-1]
return True
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
def _process_parsed(self, key_text: Dict[str, str], query: str) -> Query:
# strip all the values in key_text
key_text = {key: value.strip() for key, value in key_text.items()}
2023-06-12 12:56:14 +00:00
song = None if not "t" in key_text else Song(title=key_text["t"], dynamic=True)
album = None if not "r" in key_text else Album(title=key_text["r"], dynamic=True)
artist = None if not "a" in key_text else Artist(name=key_text["a"], dynamic=True)
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
if song is not None:
2023-07-28 07:24:14 +00:00
if album is not None:
song.album_collection.append(album)
if artist is not None:
song.artist_collection.append(artist)
2023-06-12 12:56:14 +00:00
return Query(raw_query=query, music_object=song)
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
if album is not None:
2023-07-28 07:24:14 +00:00
if artist is not None:
album.artist_collection.append(artist)
2023-06-12 12:56:14 +00:00
return Query(raw_query=query, music_object=album)
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
if artist is not None:
return Query(raw_query=query, music_object=artist)
2024-01-15 11:48:36 +00:00
2023-06-12 15:40:54 +00:00
return Query(raw_query=query)
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
def search(self, query: str):
2023-06-12 15:40:54 +00:00
if re.match(URL_PATTERN, query) is not None:
try:
page, data_object = self.pages.fetch_url(query)
except UrlNotFoundException as e:
print(f"{e.url} could not be attributed/parsed to any yet implemented site.\n"
f"PR appreciated if the site isn't implemented.\n"
f"Recommendations and suggestions on sites to implement appreciated.\n"
f"But don't be a bitch if I don't end up implementing it.")
return
2024-05-07 11:34:18 +00:00
self.set_current_options(PageResults(page, data_object.options, max_items_per_page=self.max_displayed_options))
2023-06-12 15:40:54 +00:00
self.print_current_options()
return
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
special_characters = "#\\"
query = query + " "
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
key_text = {}
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
skip_next = False
escape_next = False
new_text = ""
latest_key: str = None
for i in range(len(query) - 1):
current_char = query[i]
2024-01-15 11:48:36 +00:00
next_char = query[i + 1]
2023-06-12 12:56:14 +00:00
if skip_next:
skip_next = False
continue
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
if escape_next:
new_text += current_char
escape_next = False
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
# escaping
if current_char == "\\":
if next_char in special_characters:
escape_next = True
continue
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
if current_char == "#":
if latest_key is not None:
key_text[latest_key] = new_text
new_text = ""
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
latest_key = next_char
skip_next = True
continue
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
new_text += current_char
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
if latest_key is not None:
key_text[latest_key] = new_text
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
parsed_query: Query = self._process_parsed(key_text, query)
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
self.set_current_options(self.pages.search(parsed_query))
self.print_current_options()
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
def goto(self, data_object: DatabaseObject):
2023-06-12 12:56:14 +00:00
page: Type[Page]
2024-01-15 11:48:36 +00:00
2024-05-08 07:15:41 +00:00
self.pages.fetch_details(data_object, stop_at_level=1)
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
self.set_current_options(GoToResults(data_object.options, max_items_per_page=self.max_displayed_options))
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
self.print_current_options()
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
def download(self, data_objects: List[DatabaseObject], **kwargs) -> bool:
output()
2024-05-10 15:33:07 +00:00
if len(data_objects) > 1:
output(f"Downloading {len(data_objects)} objects...", *("- " + o.option_string for o in data_objects), color=BColors.BOLD, sep="\n")
2024-01-15 11:48:36 +00:00
2023-06-12 15:40:54 +00:00
_result_map: Dict[DatabaseObject, DownloadResult] = dict()
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
for database_object in data_objects:
r = self.pages.download(
2024-05-15 11:16:11 +00:00
data_object=database_object,
2024-05-07 11:34:18 +00:00
genre=self.genre,
**kwargs
)
2023-06-12 15:40:54 +00:00
_result_map[database_object] = r
2024-01-15 11:48:36 +00:00
2023-06-12 15:40:54 +00:00
for music_object, result in _result_map.items():
2024-05-07 11:34:18 +00:00
output()
output(music_object.option_string)
output(result)
2024-01-15 11:48:36 +00:00
2023-06-12 17:46:46 +00:00
return True
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
def process_input(self, input_str: str) -> bool:
2024-05-07 11:34:18 +00:00
try:
input_str = input_str.strip()
processed_input: str = input_str.lower()
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
if processed_input in EXIT_COMMANDS:
return True
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
if processed_input == ".":
self.print_current_options()
2024-05-07 11:34:18 +00:00
return False
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
if processed_input == "..":
if self.previous_option():
self.print_current_options()
return False
command = ""
query = processed_input
if ":" in processed_input:
_ = processed_input.split(":")
command, query = _[0], ":".join(_[1:])
do_search = "s" in command
do_fetch = "f" in command
2024-05-07 11:34:18 +00:00
do_download = "d" in command
do_merge = "m" in command
if do_search and (do_download or do_fetch or do_merge):
raise MKInvalidInputException(message="You can't search and do another operation at the same time.")
2024-05-07 11:34:18 +00:00
if do_search:
self.search(":".join(input_str.split(":")[1:]))
return False
def get_selected_objects(q: str):
if q.strip().lower() == "all":
return list(self.current_results)
indices = []
for possible_index in q.split(","):
possible_index = possible_index.strip()
if possible_index == "":
continue
i = 0
try:
i = int(possible_index)
except ValueError:
raise MKInvalidInputException(message=f"The index \"{possible_index}\" is not a number.")
if i < 0 or i >= len(self.current_results):
raise MKInvalidInputException(message=f"The index \"{i}\" is not within the bounds of 0-{len(self.current_results) - 1}.")
indices.append(i)
return [self.current_results[i] for i in indices]
selected_objects = get_selected_objects(query)
2024-05-07 11:34:18 +00:00
if do_merge:
old_selected_objects = selected_objects
a = old_selected_objects[0]
for b in old_selected_objects[1:]:
if type(a) != type(b):
raise MKInvalidInputException(message="You can't merge different types of objects.")
a.merge(b)
selected_objects = [a]
if do_fetch:
for data_object in selected_objects:
self.pages.fetch_details(data_object)
self.print_current_options()
return False
2024-05-07 11:34:18 +00:00
if do_download:
self.download(selected_objects)
return False
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
if len(selected_objects) != 1:
raise MKInvalidInputException(message="You can only go to one object at a time without merging.")
2024-01-15 11:48:36 +00:00
2024-05-07 11:34:18 +00:00
self.goto(selected_objects[0])
2023-06-12 12:56:14 +00:00
return False
2024-05-07 11:34:18 +00:00
except MKInvalidInputException as e:
output("\n" + e.message + "\n", color=BColors.FAIL)
help_message()
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
return False
2024-01-15 11:48:36 +00:00
2023-06-12 12:56:14 +00:00
def mainloop(self):
while True:
if self.process_input(input("> ")):
return
2023-06-20 14:40:34 +00:00
2024-01-15 11:48:36 +00:00
2023-06-20 14:40:34 +00:00
@cli_function
def download(
genre: str = None,
download_all: bool = False,
direct_download_url: str = None,
2023-06-20 17:30:48 +00:00
command_list: List[str] = None,
process_metadata_anyway: bool = False,
2023-06-20 14:40:34 +00:00
):
2023-09-10 14:54:06 +00:00
if main_settings["hasnt_yet_started"]:
code = initial_config()
if code == 0:
2023-09-10 14:54:06 +00:00
main_settings["hasnt_yet_started"] = False
write_config()
print(f"{BColors.OKGREEN.value}Restart the programm to use it.{BColors.ENDC.value}")
else:
print(f"{BColors.FAIL.value}Something went wrong configuring.{BColors.ENDC.value}")
2024-01-15 11:48:36 +00:00
2023-06-20 17:30:48 +00:00
shell = Downloader(genre=genre, process_metadata_anyway=process_metadata_anyway)
2024-01-15 11:48:36 +00:00
2023-06-20 14:40:34 +00:00
if command_list is not None:
for command in command_list:
shell.process_input(command)
return
if direct_download_url is not None:
if shell.download(direct_download_url, download_all=download_all):
return
2024-01-15 11:48:36 +00:00
2023-06-20 14:40:34 +00:00
shell.mainloop()