|
|
|
@@ -3,8 +3,9 @@ import random
|
|
|
|
|
import re
|
|
|
|
|
from copy import copy
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Optional, Union, Type, Dict, Set, List, Tuple
|
|
|
|
|
from typing import Optional, Union, Type, Dict, Set, List, Tuple, TypedDict
|
|
|
|
|
from string import Formatter
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
@@ -33,6 +34,18 @@ from ..utils import trace
|
|
|
|
|
INDEPENDENT_DB_OBJECTS = Union[Label, Album, Artist, Song]
|
|
|
|
|
INDEPENDENT_DB_TYPES = Union[Type[Song], Type[Album], Type[Artist], Type[Label]]
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class FetchOptions:
|
|
|
|
|
download_all: bool = False
|
|
|
|
|
album_type_blacklist: Set[AlbumType] = field(default_factory=lambda: set(AlbumType(a) for a in main_settings["album_type_blacklist"]))
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class DownloadOptions:
|
|
|
|
|
download_all: bool = False
|
|
|
|
|
album_type_blacklist: Set[AlbumType] = field(default_factory=lambda: set(AlbumType(a) for a in main_settings["album_type_blacklist"]))
|
|
|
|
|
|
|
|
|
|
process_audio_if_found: bool = False
|
|
|
|
|
process_metadata_if_found: bool = True
|
|
|
|
|
|
|
|
|
|
class NamingDict(dict):
|
|
|
|
|
CUSTOM_KEYS: Dict[str, str] = {
|
|
|
|
@@ -101,6 +114,10 @@ class Page:
|
|
|
|
|
# set this to true, if all song details can also be fetched by fetching album details
|
|
|
|
|
NO_ADDITIONAL_DATA_FROM_SONG = False
|
|
|
|
|
|
|
|
|
|
def __init__(self, download_options: DownloadOptions = None, fetch_options: FetchOptions = None):
|
|
|
|
|
self.download_options: DownloadOptions = download_options or DownloadOptions()
|
|
|
|
|
self.fetch_options: FetchOptions = fetch_options or FetchOptions()
|
|
|
|
|
|
|
|
|
|
def _search_regex(self, pattern, string, default=None, fatal=True, flags=0, group=None):
|
|
|
|
|
"""
|
|
|
|
|
Perform a regex search on the given string, using a single or a list of
|
|
|
|
@@ -176,7 +193,6 @@ class Page:
|
|
|
|
|
self,
|
|
|
|
|
music_object: DatabaseObject,
|
|
|
|
|
stop_at_level: int = 1,
|
|
|
|
|
post_process: bool = True
|
|
|
|
|
) -> DatabaseObject:
|
|
|
|
|
"""
|
|
|
|
|
when a music object with lacking data is passed in, it returns
|
|
|
|
@@ -208,7 +224,6 @@ class Page:
|
|
|
|
|
source=source,
|
|
|
|
|
enforce_type=type(music_object),
|
|
|
|
|
stop_at_level=stop_at_level,
|
|
|
|
|
post_process=False,
|
|
|
|
|
type_string=type(music_object).__name__,
|
|
|
|
|
entity_string=music_object.option_string,
|
|
|
|
|
)
|
|
|
|
@@ -230,7 +245,6 @@ class Page:
|
|
|
|
|
source: Source,
|
|
|
|
|
stop_at_level: int = 2,
|
|
|
|
|
enforce_type: Type[DatabaseObject] = None,
|
|
|
|
|
post_process: bool = True,
|
|
|
|
|
type_string: str = "",
|
|
|
|
|
entity_string: str = "",
|
|
|
|
|
) -> Optional[DatabaseObject]:
|
|
|
|
@@ -268,7 +282,7 @@ class Page:
|
|
|
|
|
|
|
|
|
|
for sub_element in collection:
|
|
|
|
|
sub_element.merge(
|
|
|
|
|
self.fetch_details(sub_element, stop_at_level=stop_at_level - 1, post_process=False))
|
|
|
|
|
self.fetch_details(sub_element, stop_at_level=stop_at_level - 1))
|
|
|
|
|
|
|
|
|
|
return music_object
|
|
|
|
|
|
|
|
|
@@ -288,8 +302,6 @@ class Page:
|
|
|
|
|
self,
|
|
|
|
|
music_object: DatabaseObject,
|
|
|
|
|
genre: str,
|
|
|
|
|
download_all: bool = False,
|
|
|
|
|
process_metadata_anyway: bool = True
|
|
|
|
|
) -> DownloadResult:
|
|
|
|
|
naming_dict: NamingDict = NamingDict({"genre": genre})
|
|
|
|
|
|
|
|
|
@@ -308,25 +320,19 @@ class Page:
|
|
|
|
|
|
|
|
|
|
fill_naming_objects(music_object)
|
|
|
|
|
|
|
|
|
|
return self._download(music_object, naming_dict, download_all, process_metadata_anyway=process_metadata_anyway)
|
|
|
|
|
return self._download(music_object, naming_dict)
|
|
|
|
|
|
|
|
|
|
def _download(
|
|
|
|
|
self,
|
|
|
|
|
music_object: DatabaseObject,
|
|
|
|
|
naming_dict: NamingDict,
|
|
|
|
|
download_all: bool = False,
|
|
|
|
|
skip_details: bool = False,
|
|
|
|
|
process_metadata_anyway: bool = True
|
|
|
|
|
**kwargs
|
|
|
|
|
) -> DownloadResult:
|
|
|
|
|
trace(f"downloading {type(music_object).__name__} [{music_object.option_string}]")
|
|
|
|
|
skip_next_details = skip_details
|
|
|
|
|
|
|
|
|
|
# Skips all releases, that are defined in shared.ALBUM_TYPE_BLACKLIST, if download_all is False
|
|
|
|
|
if isinstance(music_object, Album):
|
|
|
|
|
if self.NO_ADDITIONAL_DATA_FROM_SONG:
|
|
|
|
|
skip_next_details = True
|
|
|
|
|
|
|
|
|
|
if not download_all and music_object.album_type.value in main_settings["album_type_blacklist"]:
|
|
|
|
|
if not self.download_options.download_all and music_object.album_type in self.download_options.album_type_blacklist:
|
|
|
|
|
return DownloadResult()
|
|
|
|
|
|
|
|
|
|
if not (isinstance(music_object, Song) and self.NO_ADDITIONAL_DATA_FROM_SONG):
|
|
|
|
@@ -338,7 +344,7 @@ class Page:
|
|
|
|
|
naming_dict.add_object(music_object)
|
|
|
|
|
|
|
|
|
|
if isinstance(music_object, Song):
|
|
|
|
|
return self._download_song(music_object, naming_dict, process_metadata_anyway=process_metadata_anyway)
|
|
|
|
|
return self._download_song(music_object, naming_dict)
|
|
|
|
|
|
|
|
|
|
download_result: DownloadResult = DownloadResult()
|
|
|
|
|
|
|
|
|
@@ -347,13 +353,11 @@ class Page:
|
|
|
|
|
|
|
|
|
|
sub_ordered_music_object: DatabaseObject
|
|
|
|
|
for sub_ordered_music_object in collection:
|
|
|
|
|
download_result.merge(self._download(sub_ordered_music_object, naming_dict.copy(), download_all,
|
|
|
|
|
skip_details=skip_next_details,
|
|
|
|
|
process_metadata_anyway=process_metadata_anyway))
|
|
|
|
|
download_result.merge(self._download(sub_ordered_music_object, naming_dict.copy()))
|
|
|
|
|
|
|
|
|
|
return download_result
|
|
|
|
|
|
|
|
|
|
def _download_song(self, song: Song, naming_dict: NamingDict, process_metadata_anyway: bool = True):
|
|
|
|
|
def _download_song(self, song: Song, naming_dict: NamingDict):
|
|
|
|
|
if "genre" not in naming_dict and song.genre is not None:
|
|
|
|
|
naming_dict["genre"] = song.genre
|
|
|
|
|
|
|
|
|
@@ -386,16 +390,14 @@ class Page:
|
|
|
|
|
target: Target
|
|
|
|
|
for target in song.target_collection:
|
|
|
|
|
if target.exists:
|
|
|
|
|
if process_metadata_anyway:
|
|
|
|
|
target.copy_content(temp_target)
|
|
|
|
|
target.copy_content(temp_target)
|
|
|
|
|
found_on_disc = True
|
|
|
|
|
|
|
|
|
|
r.found_on_disk += 1
|
|
|
|
|
r.add_target(target)
|
|
|
|
|
|
|
|
|
|
if found_on_disc and not process_metadata_anyway:
|
|
|
|
|
self.LOGGER.info(f"{song.option_string} already exists, thus not downloading again.")
|
|
|
|
|
return r
|
|
|
|
|
if found_on_disc:
|
|
|
|
|
self.LOGGER.info(f"Found {song.option_string} in the library.")
|
|
|
|
|
|
|
|
|
|
skip_intervals = []
|
|
|
|
|
if not found_on_disc:
|
|
|
|
@@ -411,16 +413,19 @@ class Page:
|
|
|
|
|
song=song,
|
|
|
|
|
temp_target=temp_target,
|
|
|
|
|
interval_list=skip_intervals,
|
|
|
|
|
found_on_disc=found_on_disc,
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
return r
|
|
|
|
|
|
|
|
|
|
def _post_process_targets(self, song: Song, temp_target: Target, interval_list: List) -> DownloadResult:
|
|
|
|
|
correct_codec(temp_target, interval_list=interval_list)
|
|
|
|
|
def _post_process_targets(self, song: Song, temp_target: Target, interval_list: List, found_on_disc: bool) -> DownloadResult:
|
|
|
|
|
if found_on_disc and self.download_options.process_audio_if_found:
|
|
|
|
|
correct_codec(temp_target, interval_list=interval_list)
|
|
|
|
|
|
|
|
|
|
self.post_process_hook(song, temp_target)
|
|
|
|
|
|
|
|
|
|
write_metadata_to_target(song.metadata, temp_target, song)
|
|
|
|
|
if found_on_disc and self.download_options.process_metadata_if_found:
|
|
|
|
|
write_metadata_to_target(song.metadata, temp_target, song)
|
|
|
|
|
|
|
|
|
|
r = DownloadResult()
|
|
|
|
|
|
|
|
|
|