コード例 #1
0
 def __init__(self):
     self.bitrate = None
     self.alerts = False
     self.record_type = None
     self.download_path = None
     self.remove = False
     self.refresh = True
     self.is_search = False
     self.duplicates = 0
     self.time_machine = None
     self.dl = None
     self.db = Database()
     self.api = PlatformAPI()
コード例 #2
0
ファイル: dmi.py プロジェクト: digitalec/deemon
    def __init__(self):
        logger.debug("Initializing deemix library")
        self.db = Database()
        self.dz = Deezer()

        if config.deemix_path() == "":
            self.config_dir = localpaths.getConfigFolder()
        else:
            self.config_dir = Path(config.deemix_path())

        self.dx_settings = LoadSettings(self.config_dir)

        logger.debug("deemix " + deemix.__version__)
        logger.debug(f"deemix config path: {self.config_dir}")
コード例 #3
0
 def __init__(self, profile_name):
     self.db = Database()
     self.profile_name = profile_name
     self.profile = None
コード例 #4
0
class ProfileConfig:
    def __init__(self, profile_name):
        self.db = Database()
        self.profile_name = profile_name
        self.profile = None

    # TODO move this to utils
    @staticmethod
    def print_header(message: str = None):
        print("deemon Profile Editor")
        if message:
            print(":: " + message + "\n")
        else:
            print("")

    def edit(self):
        profile = self.db.get_profile(self.profile_name)
        self.print_header(
            f"Configuring '{profile['name']}' (Profile ID: {profile['id']})")
        modified = 0
        for property in profile:
            if property == "id":
                continue
            allowed_opts = config.allowed_values(property)
            if isinstance(allowed_opts, dict):
                allowed_opts = [str(x.lower()) for x in allowed_opts.values()]

            while True:
                friendly_text = property.replace("_", " ").title()
                user_input = input(
                    f"{friendly_text} [{profile[property]}]: ").lower()
                if user_input == "":
                    break
                # TODO move to function to share with Config.set()?
                elif user_input == "false" or user_input == "0":
                    user_input = False
                elif user_input == "true" or user_input == "1":
                    user_input = True
                elif property == "name" and self.profile_name != user_input:
                    if self.db.get_profile(user_input):
                        print("Name already in use")
                        continue
                if user_input == "none" and property != "name":
                    user_input = None
                elif allowed_opts:
                    if user_input not in allowed_opts:
                        print(f"Allowed options: " +
                              ', '.join(str(x) for x in allowed_opts))
                        continue
                logger.debug(f"User set {property} to {user_input}")
                profile[property] = user_input
                modified += 1
                break

        if modified > 0:
            user_input = input("\n:: Save these settings? [y|N] ")
            if user_input.lower() != "y":
                logger.info("No changes made, exiting...")
            else:
                self.db.update_profile(profile)
                print(f"\nProfile '{profile['name']}' has been updated!")
        else:
            print("No changes made, exiting...")

    def add(self):
        new_profile = {}
        profile_config = self.db.get_profile(self.profile_name)
        if profile_config:
            return logger.error(f"Profile {self.profile_name} already exists")
        else:
            logger.info("Adding new profile: " + self.profile_name)
            print(
                "** Any option left blank will fallback to global config **\n")
            new_profile['name'] = self.profile_name

        menu = [
            {
                'setting': 'email',
                'type': str,
                'text': 'Email address',
                'allowed': []
            },
            {
                'setting': 'alerts',
                'type': bool,
                'text': 'Alerts',
                'allowed': config.allowed_values('alerts')
            },
            {
                'setting': 'bitrate',
                'type': str,
                'text': 'Bitrate',
                'allowed': config.allowed_values('bitrate').values()
            },
            {
                'setting': 'record_type',
                'type': str,
                'text': 'Record Type',
                'allowed': config.allowed_values('record_type')
            },
            {
                'setting': 'plex_baseurl',
                'type': str,
                'text': 'Plex Base URL',
                'allowed': []
            },
            {
                'setting': 'plex_token',
                'type': str,
                'text': 'Plex Token',
                'allowed': []
            },
            {
                'setting': 'plex_library',
                'type': str,
                'text': 'Plex Library',
                'allowed': []
            },
            {
                'setting': 'download_path',
                'type': str,
                'text': 'Download Path',
                'allowed': []
            },
        ]

        for m in menu:
            repeat = True
            while repeat:
                i = input(m['text'] + ": ")
                if i == "":
                    new_profile[m['setting']] = None
                    break
                if not isinstance(i, m['type']):
                    try:
                        i = int(i)
                    except ValueError:
                        print(" - Allowed options: " +
                              ', '.join(str(x) for x in m['allowed']))
                        continue
                if len(m['allowed']) > 0:
                    if i not in m['allowed']:
                        print(" - Allowed options: " +
                              ', '.join(str(x) for x in m['allowed']))
                        continue
                new_profile[m['setting']] = i
                break

        print("\n")
        i = input(":: Save these settings? [y|N] ")
        if i.lower() != "y":
            return logger.info("Operation cancelled. No changes saved.")
        else:
            self.db.create_profile(new_profile)
            logger.debug(
                f"New profile created with the following configuration: {new_profile}"
            )

    def delete(self):
        profile_config = self.db.get_profile(self.profile_name)
        if not profile_config:
            return logger.error(f"Profile {self.profile_name} not found")

        if profile_config['id'] == 1:
            return logger.info("You cannot delete the default profile.")

        i = input(f":: Remove the profile '{self.profile_name}'? [y|N] ")
        if i.lower() == "y":
            self.db.delete_profile(self.profile_name)
            return logger.info("Profile " + self.profile_name + " deleted.")
        else:
            return logger.info("Operation cancelled")

    def show(self):
        if not self.profile_name:
            profile = self.db.get_all_profiles()
            self.print_header(f"Showing all profiles")
        else:
            profile = [self.db.get_profile(self.profile_name)]
            self.print_header(
                f"Showing profile '{profile[0]['name']}' (Profile ID: {profile[0]['id']})"
            )
            if len(profile) == 0:
                return logger.error(f"Profile {self.profile_name} not found")

        print("{:<10} {:<40} {:<8} {:<8} {:<8} {:<25} "
              "{:<20} {:<20} {:<20}".format('Name', 'Email', 'Alerts',
                                            'Bitrate', 'Type', 'Plex Base URL',
                                            'Plex Token', 'Plex Library',
                                            'Download Path'))
        for u in profile:
            id, name, email, alerts, bitrate, rtype, url, token, \
            lib, dl_path = [x if x is not None else '' for x in u.values()]
            print("{:<10} {:<40} {:<8} {:<8} {:<8} {:<25} "
                  "{:<20} {:<20} {:<20}".format(name, email, alerts, bitrate,
                                                rtype, url, token, lib,
                                                dl_path))
            print("")

    def clear(self):
        profile = self.db.get_profile(self.profile_name)
        self.print_header(
            f"Configuring '{profile['name']}' (Profile ID: {profile['id']})")
        if not profile:
            return logger.error(f"Profile {self.profile_name} not found")

        for value in profile:
            if value in ["id", "name"]:
                continue
            profile[value] = None
        self.db.update_profile(profile)
        logger.info("All values have been cleared.")
コード例 #5
0
def run(whats_new, verbose, profile):
    """Monitoring and alerting tool for new music releases using the Deezer API.

    deemon is a free and open source tool. To report issues or to contribute,
    please visit https://github.com/digitalec/deemon
    """
    global logger
    global config
    global db

    setup_logger(log_level='DEBUG' if verbose else 'INFO', log_file=startup.get_log_file())
    logger = logging.getLogger(__name__)
    logger.debug(f"deemon {__version__}")
    logger.debug(f"command: \"{' '.join([x for x in sys.argv[1:]])}\"")
    logger.debug("Python " + platform.python_version())
    logger.debug(platform.platform())
    logger.debug(f"deemon appdata is located at {startup.get_appdata_dir()}")
    
    if whats_new:
        return startup.get_changelog(__version__)

    config = Config()
    db = Database()

    db.do_upgrade()
    tid = db.get_next_transaction_id()
    config.set('tid', tid, validate=False)

    if profile:
        profile_config = db.get_profile(profile)
        if profile_config:
            LoadProfile(profile_config)
        else:
            logger.error(f"Profile {profile} does not exist.")
            sys.exit(1)
    else:
        profile_config = db.get_profile_by_id(1)
        if profile_config:
            LoadProfile(profile_config)

    if not any(x in sys.argv[1:] for x in ['-h', '--help']):
        last_checked: int = int(db.last_update_check())
        next_check: int = last_checked + (config.check_update() * 86400)
        if config.release_channel() != db.get_release_channel()['value']:
            # If release_channel has changed, check for latest release
            logger.debug(f"Release channel changed to '{config.release_channel()}'")
            db.set_release_channel()
            last_checked = 0
        if time.time() >= next_check or last_checked == 0:
            logger.info(f"Checking for updates ({config.release_channel()})...")
            config.set('update_available', 0, False)
            latest_ver = str(startup.get_latest_version(config.release_channel()))
            if latest_ver:
                db.set_latest_version(latest_ver)
            db.set_last_update_check()
        new_version = db.get_latest_ver()
        if parse_version(new_version) > parse_version(__version__):
            if parse_version(new_version).major > parse_version(__version__).major:
                config.set('update_available', new_version, False)
                print("*" * 80)
                logger.info(f"deemon {parse_version(new_version).major} is available. "
                            f"Please see the release notes before upgrading.")
                logger.info("Release notes available at: https://github.com/digitalec/deemon/releases")
                print("*" * 80)
            else:
                config.set('update_available', new_version, False)
                print("*" * 50)
                logger.info(f"* New version is available: v{__version__} -> v{new_version}")
                if config.release_channel() == "beta":
                    logger.info("* To upgrade, run `pip install --upgrade --pre deemon`")
                else:
                    logger.info("* To upgrade, run `pip install --upgrade deemon`")
                print("*" * 50)
                print("")

    config.set("start_time", int(time.time()), False)
コード例 #6
0
ファイル: rollback.py プロジェクト: digitalec/deemon
import logging

from deemon.core.db import Database
from deemon.utils import dates

logger = logging.getLogger(__name__)
db = Database()


def view_transactions():
    transactions = db.get_transactions()
    if not transactions:
        return logger.info("No transactions are available to be rolled back.")

    for i, transaction in enumerate(transactions, start=1):
        release_id = []
        artist_names = []
        playlist_titles = []

        for k, v in transaction.items():
            if (k == "releases" or k == "playlist_tracks") and transaction[k]:
                for item in transaction[k]:
                    for key, val in item.items():
                        if key == "album_id" or key == "track_id":
                            if item[key] not in release_id:
                                release_id.append(item[key])
            if k == "monitor" and transaction[k]:
                if transaction[k] not in artist_names:
                    artist_names = [x['artist_name'] for x in transaction[k]]
            if k == "playlists" and transaction[k]:
                if transaction[k] not in playlist_titles:
コード例 #7
0
ファイル: show.py プロジェクト: digitalec/deemon
 def __init__(self):
     self.db = Database()
コード例 #8
0
ファイル: show.py プロジェクト: digitalec/deemon
class Show:
    def __init__(self):
        self.db = Database()

    def monitoring(self,
                   artist: bool = True,
                   query: str = None,
                   export_csv: bool = False,
                   save_path: Union[str, Path] = None,
                   filter: str = None,
                   hide_header: bool = False,
                   is_id: bool = False,
                   backup: Union[str, Path] = None):
        def csv_output(line: str):
            if save_path:
                output_to_file.append(line)
            else:
                print(line)

        output_to_file = []

        if backup:
            export_csv = True
            filter = "id"
            hide_header = True
            save_path = backup

        if artist:
            if query:
                db_result = self.db.get_monitored_artist_by_name(query)
            else:
                db_result = self.db.get_all_monitored_artists()

            if not db_result:
                if query:
                    return logger.error("Artist not found: " + str(query))
                else:
                    return logger.error("No artists are being monitored")
        else:
            if query:
                if is_id:
                    try:
                        query = int(query)
                    except ValueError:
                        return logger.error(f"Invalid Playlist ID - {query}")
                    db_result = self.db.get_monitored_playlist_by_id(query)
                else:
                    db_result = self.db.get_monitored_playlist_by_name(query)
            else:
                db_result = self.db.get_all_monitored_playlists()

            if not db_result:
                if query:
                    return logger.error("Playlist/ID not found: " + str(query))
                else:
                    return logger.error("No playlists are being monitored")

        if artist and query:
            for key, val in db_result.items():
                if val == None:
                    db_result[key] = "-"

            print("{:<10} {:<35} {:<10} {:<10} {:<10} {:<25}".format(
                'ID', 'Artist', 'Alerts', 'Bitrate', 'Type', 'Download Path'))

            print(
                "{!s:<10} {!s:<35} {!s:<10} {!s:<10} {!s:<10} {!s:<25}".format(
                    db_result['artist_id'], db_result['artist_name'],
                    db_result['alerts'], db_result['bitrate'],
                    db_result['record_type'], db_result['download_path']))
            print("")
        elif not artist and query:
            for key, val in db_result.items():
                if val == None:
                    db_result[key] = "-"

            print("{:<15} {:<30} {:<50} {:<10} {:<10} {:<25}".format(
                'ID', 'Title', 'URL', 'Alerts', 'Bitrate', 'Download Path'))

            print("{!s:<15} {!s:<30} {!s:<50}  {!s:<10} {!s:<10} {!s:<25}".
                  format(db_result['id'], db_result['title'], db_result['url'],
                         db_result['alerts'], db_result['bitrate'],
                         db_result['download_path']))
            print("")
        else:
            if export_csv or save_path:
                if artist:
                    if not filter:
                        filter = "name,id,bitrate,alerts,type,path"
                    filter = filter.split(',')
                    logger.debug(
                        f"Generating CSV data using filters: {', '.join(filter)}"
                    )
                    column_names = [
                        'artist_id' if x == 'id' else x for x in filter
                    ]
                    column_names = [
                        'artist_name' if x == 'name' else x
                        for x in column_names
                    ]
                    column_names = [
                        'record_type' if x == 'type' else x
                        for x in column_names
                    ]
                    column_names = [
                        'download_path' if x == 'path' else x
                        for x in column_names
                    ]
                else:
                    if not filter:
                        filter = "id,title,url,bitrate,alerts,path"
                    filter = filter.split(',')
                    logger.debug(
                        f"Generating CSV data using filters: {', '.join(filter)}"
                    )
                    column_names = [
                        'download_path' if x == 'path' else x for x in filter
                    ]

                for column in column_names:
                    if not len([x for x in db_result if column in x.keys()]):
                        logger.warning(f"Unknown filter specified: {column}")
                        column_names.remove(column)

                if not hide_header:
                    csv_output(','.join(filter))
                for artist in db_result:
                    filtered_artists = []
                    for key, value in artist.items():
                        if value is None:
                            artist[key] = ""
                    for column in column_names:
                        filtered_artists.append(str(artist[column]))
                    if len(filtered_artists):
                        for i, a in enumerate(filtered_artists):
                            if '"' in a:
                                a = a.replace('"', "'")
                            if ',' in a:
                                filtered_artists[i] = f'"{a}"'
                        csv_output(",".join(filtered_artists))

                if output_to_file:
                    if Path(save_path).is_dir():
                        output_filename = Path(
                            save_path /
                            f"{generate_date_filename('deemon_')}.csv")
                    else:
                        output_filename = Path(save_path)

                    with open(output_filename, 'w', encoding="utf-8") as f:
                        for line in output_to_file:
                            if line == output_to_file[-1]:
                                f.writelines(line)
                                break
                            f.writelines(line + "\n")

                    return logger.info("CSV data has been saved to: " +
                                       str(output_filename))

                return

            if artist:
                db_result = [x['artist_name'] for x in db_result]
            else:
                db_result = [x['title'] for x in db_result]
            if len(db_result) < 10:
                for artist in db_result:
                    print(artist)
            else:
                db_result = self.truncate_long_artists(db_result)

                try:
                    size = os.get_terminal_size()
                    max_cols = (int(size.columns / 30))
                except:
                    max_cols = 5

                if max_cols > 5:
                    max_cols = 5

                while len(db_result) % max_cols != 0:
                    db_result.append(" ")

                if max_cols >= 5:
                    for a, b, c, d, e in zip(db_result[0::5], db_result[1::5],
                                             db_result[2::5], db_result[3::5],
                                             db_result[4::5]):
                        print('{:<30}{:<30}{:<30}{:<30}{:<30}'.format(
                            a, b, c, d, e))
                elif max_cols >= 4:
                    for a, b, c, d in zip(db_result[0::4], db_result[1::4],
                                          db_result[2::4], db_result[3::4]):
                        print('{:<30}{:<30}{:<30}{:<30}'.format(a, b, c, d))
                elif max_cols >= 3:
                    for a, b, c in zip(db_result[0::3], db_result[1::3],
                                       db_result[2::3]):
                        print('{:<30}{:<30}{:<30}'.format(a, b, c))
                elif max_cols >= 2:
                    for a, b in zip(db_result[0::2], db_result[1::2]):
                        print('{:<30}{:<30}'.format(a, b))
                else:
                    for a in db_result:
                        print(a)

    def playlists(self, csv=False):
        monitored_playlists = self.db.get_all_monitored_playlists()
        for p in monitored_playlists:
            print(f"{p[1]} ({p[2]})")

    @staticmethod
    def truncate_long_artists(all_artists):
        for idx, artist in enumerate(all_artists):
            if len(artist) > 25:
                all_artists[idx] = artist[:22] + "..."
            all_artists[idx] = artist
        return all_artists

    def releases(self, days, future):
        if future:
            future_releases = self.db.get_future_releases()
            future_release_list = [x for x in future_releases]
            if len(future_release_list) > 0:
                logger.info(f"Future releases:")
                print("")
                future_release_list.sort(key=lambda x: x['album_release'],
                                         reverse=True)
                for release in future_release_list:
                    print('+ [%-10s] %s - %s' %
                          (release['album_release'], release['artist_name'],
                           release['album_name']))
            else:
                logger.info("No future releases have been detected")
        else:
            seconds_per_day = 86400
            days_in_seconds = (days * seconds_per_day)
            now = int(time.time())
            back_date = (now - days_in_seconds)
            releases = self.db.show_new_releases(back_date, now)
            release_list = [x for x in releases]
            if len(release_list) > 0:
                logger.info(f"New releases found within last {days} day(s):")
                print("")
                release_list.sort(key=lambda x: x['album_release'],
                                  reverse=True)
                for release in release_list:
                    print('+ [%-10s] %s - %s' %
                          (release['album_release'], release['artist_name'],
                           release['album_name']))
            else:
                logger.info(f"No releases found in the last {days} day(s)")
コード例 #9
0
ファイル: dmi.py プロジェクト: digitalec/deemon
class DeemixInterface:
    def __init__(self):
        logger.debug("Initializing deemix library")
        self.db = Database()
        self.dz = Deezer()

        if config.deemix_path() == "":
            self.config_dir = localpaths.getConfigFolder()
        else:
            self.config_dir = Path(config.deemix_path())

        self.dx_settings = LoadSettings(self.config_dir)

        logger.debug("deemix " + deemix.__version__)
        logger.debug(f"deemix config path: {self.config_dir}")

    def download_url(self, url, bitrate, download_path, override_deemix=True):
        if override_deemix:
            deemix.generatePlaylistItem = self.generatePlaylistItem

        if download_path:
            self.dx_settings['downloadLocation'] = download_path
            logger.debug(f"deemix download path set to: {self.dx_settings['downloadLocation']}")

        links = []
        for link in url:
            if ';' in link:
                for l in link.split(";"):
                    links.append(l)
            else:
                links.append(link)
        for link in links:
            download_object = generateDownloadObject(self.dz, link, bitrate)
            if isinstance(download_object, list):
                for obj in download_object:
                    Downloader(self.dz, obj, self.dx_settings).start()
            else:
                Downloader(self.dz, download_object, self.dx_settings).start()

    def deezer_acct_type(self):
        user_session = self.dz.get_session()['current_user']
    
        if user_session.get('can_stream_lossless'):
            logger.debug("Deezer account connected and supports lossless")
            config.set('deezer_quality', 'lossless', validate=False)
        elif user_session.get('can_stream_hq'):
            logger.debug("Deezer account connected and supports high quality")
            config.set('deezer_quality', 'hq', validate=False)
        else:
            logger.warning("Deezer account connected but only supports 128k")
            config.set('deezer_quality', 'lq', validate=False)

    def verify_arl(self, arl):
        if not self.dz.login_via_arl(arl):
            print("FAILED")
            logger.debug(f"ARL Failed: {arl}")
            return False
        self.deezer_acct_type()
            
        print("OK")
        logger.debug("ARL is valid")
        return True

    def login(self):
        failed_logins = 0
        logger.debug("Looking for ARL...")
        if config.arl():
            logger.debug("ARL found in deemon config")
            print(":: Found ARL in deemon config, checking... ", end="")
            if self.verify_arl(config.arl()):
                return True
            else:
                logger.error("Unable to login using ARL found in deemon config")
                failed_logins += 1
        else:
            logger.debug("ARL was not found in deemon config, checking if deemix has it...")

        if self.config_dir.is_dir():
            if Path(self.config_dir / '.arl').is_file():
                with open(self.config_dir / '.arl', 'r') as f:
                    arl_from_file = f.readline().rstrip("\n")
                    logger.debug("ARL found in deemix config")
                    print(":: Found ARL in deemix .arl file, checking... ", end="")
                    if self.verify_arl(arl_from_file):
                        return True
                    else:
                        logger.error("Unable to login using ARL found in deemix config directory")
                        failed_logins += 1
            else:
                logger.debug(f"ARL not found in {self.config_dir}")
        else:
            logger.error(f"ARL directory {self.config_dir} was not found")

        if failed_logins > 1:
            notification = notifier.Notify()
            notification.expired_arl()
        else:
            logger.error("No ARL was found, aborting...")
            return False

    def generatePlaylistItem(self, dz, link_id, bitrate, playlistAPI=None, playlistTracksAPI=None):
        if not playlistAPI:
            if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/playlist/{link_id}")
            # Get essential playlist info
            try:
                playlistAPI = dz.api.get_playlist(link_id)
            except APIError:
                playlistAPI = None
            # Fallback to gw api if the playlist is private
            if not playlistAPI:
                try:
                    userPlaylist = dz.gw.get_playlist_page(link_id)
                    playlistAPI = map_user_playlist(userPlaylist['DATA'])
                except GWAPIError as e:
                    raise GenerationError(f"https://deezer.com/playlist/{link_id}", str(e)) from e

            # Check if private playlist and owner
            if not playlistAPI.get('public', False) and playlistAPI['creator']['id'] != str(self.dz.current_user['id']):
                logger.warning("You can't download others private playlists.")
                raise NotYourPrivatePlaylist(f"https://deezer.com/playlist/{link_id}")

        if not playlistTracksAPI:
            playlistTracksAPI = dz.gw.get_playlist_tracks(link_id)
        playlistAPI['various_artist'] = dz.api.get_artist(5080)  # Useful for save as compilation

        totalSize = len(playlistTracksAPI)
        playlistAPI['nb_tracks'] = totalSize
        collection = []
        for pos, trackAPI in enumerate(playlistTracksAPI, start=1):
            #
            # BEGIN DEEMON PATCH
            #
            vals = {'track_id': trackAPI['SNG_ID'], 'playlist_id': playlistAPI['id']}
            sql = "SELECT * FROM 'playlist_tracks' WHERE track_id = :track_id AND playlist_id = :playlist_id"
            result = self.db.query(sql, vals).fetchone()
            if result:
                continue
            #
            # END DEEMON PATCH
            #
            trackAPI = map_track(trackAPI)
            if trackAPI['explicit_lyrics']:
                playlistAPI['explicit'] = True
            if 'track_token' in trackAPI: del trackAPI['track_token']
            trackAPI['position'] = pos
            collection.append(trackAPI)

        if 'explicit' not in playlistAPI: playlistAPI['explicit'] = False

        return Collection({
            'type': 'playlist',
            'id': link_id,
            'bitrate': bitrate,
            'title': playlistAPI['title'],
            'artist': playlistAPI['creator']['name'],
            'cover': playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg',
            'explicit': playlistAPI['explicit'],
            'size': totalSize,
            'collection': {
                'tracks': collection,
                'playlistAPI': playlistAPI
            }
        })
コード例 #10
0
class Monitor:

    def __init__(self):
        self.bitrate = None
        self.alerts = False
        self.record_type = None
        self.download_path = None
        self.remove = False
        self.refresh = True
        self.is_search = False
        self.duplicates = 0
        self.time_machine = None
        self.dl = None
        self.db = Database()
        self.api = PlatformAPI()

    def set_config(self, bitrate: str, alerts: bool, record_type: str, download_path: Path):
        self.bitrate = bitrate
        self.alerts = alerts
        self.record_type = record_type
        self.download_path = download_path
        self.debugger("SetConfig", {'bitrate': bitrate, 'alerts': alerts, 'type': record_type, 'path': download_path})

    def set_options(self, remove, dl_all, search):
        self.remove = True if remove else False
        self.dl = True if dl_all else False
        self.is_search = True if search else False
        self.debugger("SetOptions", {'remove': remove, 'dl': dl_all, 'search': search})

    def debugger(self, message: str, payload=None):
        if config.debug_mode():
            if not payload:
                payload = ""
            logger.debug(f"DEBUG_MODE: {message} {str(payload)}")

    def get_best_result(self, api_result) -> list:
        name = api_result['query']

        if self.is_search:
            logger.debug("Waiting for user input...")
            prompt = self.prompt_search(name, api_result['results'])
            if prompt:
                logger.debug(f"User selected {prompt}")
                return [prompt]

        matches = [r for r in api_result['results'] if r['name'].lower() == name.lower()]
        self.debugger("Matches", matches)

        if len(matches) == 1:
            return [matches[0]]
        elif len(matches) > 1:
            logger.debug(f"Multiple matches were found for artist \"{api_result['query']}\"")
            if config.prompt_duplicates():
                logger.debug("Waiting for user input...")
                prompt = self.prompt_search(name, matches)
                if prompt:
                    logger.debug(f"User selected {prompt}")
                    return [prompt]
                else:
                    logger.info(f"No selection made, skipping {name}...")
                    return []
            else:
                self.duplicates += 1
                return [matches[0]]
        elif not len(matches):
            logger.debug(f"   [!] No matches were found for artist \"{api_result['query']}\"")
            if config.prompt_no_matches() and len(api_result['results']):
                logger.debug("Waiting for user input...")
                prompt = self.prompt_search(name, api_result['results'])
                if prompt:
                    logger.debug(f"User selected {prompt}")
                    return [prompt]
                else:
                    logger.info(f"No selection made, skipping {name}...")
                    return []
            else:
                logger.info(f"   [!] Artist {name} not found")
                return []

    def prompt_search(self, value, api_result):
        menu = search.Search()
        ask_user = menu.artist_menu(value, api_result, True)
        if ask_user:
            return {'id': ask_user['id'], 'name': ask_user['name']}
        return logger.debug("No artist selected, skipping...")

    # @performance.timeit
    def build_artist_query(self, api_result: list):
        existing = self.db.get_all_monitored_artist_ids()
        artists_to_add = []
        pbar = tqdm(api_result, total=len(api_result), desc="Setting up artists for monitoring...", ascii=" #",
                    bar_format=ui.TQDM_FORMAT)
        for artist in pbar:
            if artist is None:
                continue
            if artist['id'] in existing:
                logger.info(f"   - Already monitoring {artist['name']}, skipping...")
            else:
                artist.update({'bitrate': self.bitrate, 'alerts': self.alerts, 'record_type': self.record_type,
                               'download_path': self.download_path, 'profile_id': config.profile_id(),
                               'trans_id': config.transaction_id()})
                artists_to_add.append(artist)
        if len(artists_to_add):
            logger.debug("New artists have been monitored. Saving changes to the database...")
            self.db.new_transaction()
            self.db.fast_monitor(artists_to_add)
            self.db.commit()
            return True

    def build_playlist_query(self, api_result: list):
        existing = self.db.get_all_monitored_playlist_ids() or []
        playlists_to_add = []
        pbar = tqdm(api_result, total=len(api_result), desc="Setting up playlists for monitoring...", ascii=" #",
                    bar_format=ui.TQDM_FORMAT)
        for i, playlist in enumerate(pbar):
            if not playlist:
                continue
            if playlist['id'] in existing:
                logger.info(f"   Already monitoring {playlist['title']}, skipping...")
            else:
                playlist.update({'bitrate': self.bitrate, 'alerts': self.alerts, 'download_path': self.download_path,
                                 'profile_id': config.profile_id(), 'trans_id': config.transaction_id()})
                playlists_to_add.append(playlist)
        if len(playlists_to_add):
            logger.debug("New playlists have been monitored. Saving changes to the database...")
            self.db.new_transaction()
            self.db.fast_monitor_playlist(playlists_to_add)
            self.db.commit()
            return True

    def call_refresh(self):
        refresh = Refresh(self.time_machine, ignore_filters=self.dl)
        refresh.run()

    # @performance.timeit
    def artists(self, names: list) -> None:
        """
        Return list of dictionaries containing each artist
        """
        if self.remove:
            return self.purge_artists(names=names)
        self.debugger("SpawningThreads", self.api.max_threads)
        with ThreadPoolExecutor(max_workers=self.api.max_threads) as ex:
            api_result = list(
                tqdm(ex.map(self.api.search_artist, names), total=len(names),
                     desc=f"Fetching artist data for {len(names):,} artist(s), please wait...",
                     ascii=" #", bar_format=ui.TQDM_FORMAT))

        select_artist = tqdm(api_result, total=len(api_result), desc="Examining results for best match...", ascii=" #",
                             bar_format=ui.TQDM_FORMAT)

        to_monitor = []
        for artist in select_artist:
            best_result = self.get_best_result(artist)
            if best_result:
                to_monitor.append(best_result)

        to_process = [item for elem in to_monitor for item in elem if len(item)]
        if self.build_artist_query(to_process):
            self.call_refresh()
        else:
            print("")
            logger.info("No new artists have been added, skipping refresh.")

    # @performance.timeit
    def artist_ids(self, ids: list):
        ids = [int(x) for x in ids]
        if self.remove:
            return self.purge_artists(ids=ids)
        self.debugger("SpawningThreads", self.api.max_threads)
        with ThreadPoolExecutor(max_workers=self.api.max_threads) as ex:
            api_result = list(
                tqdm(ex.map(self.api.get_artist_by_id, ids), total=len(ids),
                     desc=f"Fetching artist data for {len(ids):,} artist(s), please wait...",
                     ascii=" #", bar_format=ui.TQDM_FORMAT))

        if self.build_artist_query(api_result):
            self.call_refresh()
        else:
            print("")
            logger.info("No new artists have been added, skipping refresh.")

    # @performance.timeit
    def importer(self, import_path: str):
        if Path(import_path).is_file():
            imported_file = dataprocessor.read_file_as_csv(import_path)
            artist_list = dataprocessor.process_input_file(imported_file)
            if isinstance(artist_list[0], int):
                self.artist_ids(artist_list)
            else:
                self.artists(artist_list)
        elif Path(import_path).is_dir():
            import_list = [x.relative_to(import_path).name for x in sorted(Path(import_path).iterdir()) if x.is_dir()]
            if import_list:
                self.artists(import_list)
        else:
            logger.error(f"File or directory not found: {import_path}")
            return

    # @performance.timeit
    def playlists(self, playlists: list):
        if self.remove:
            return self.purge_playlists(ids=playlists)
        ids = [int(x) for x in playlists]
        self.debugger("SpawningThreads", self.api.max_threads)
        with ThreadPoolExecutor(max_workers=self.api.max_threads) as ex:
            api_result = list(
                tqdm(ex.map(self.api.get_playlist, ids), total=len(ids),
                     desc=f"Fetching playlist data for {len(ids):,} playlist(s), please wait...",
                     ascii=" #", bar_format=ui.TQDM_FORMAT))

        if self.build_playlist_query(api_result):
            self.call_refresh()
        else:
            print("")
            logger.info("No new playlists have been added, skipping refresh.")

    # @performance.timeit
    def purge_artists(self, names: list = None, ids: list = None):
        if names:
            for n in names:
                monitored = self.db.get_monitored_artist_by_name(n)
                if monitored:
                    self.db.remove_monitored_artist(monitored['artist_id'])
                    logger.info(f"No longer monitoring {monitored['artist_name']}")
                else:
                    logger.info(f"{n} is not being monitored yet")
        if ids:
            for i in ids:
                monitored = self.db.get_monitored_artist_by_id(i)
                if monitored:
                    self.db.remove_monitored_artist(monitored['artist_id'])
                    logger.info(f"\nNo longer monitoring {monitored['artist_name']}")
                else:
                    logger.info(f"{i} is not being monitored yet")

    def purge_playlists(self, titles: list = None, ids: list = None):
        if ids:
            for i in ids:
                monitored = self.db.get_monitored_playlist_by_id(i)
                if monitored:
                    self.db.remove_monitored_playlists(monitored['id'])
                    logger.info(f"\nNo longer monitoring {monitored['title']}")
                else:
                    logger.info(f"{i} is not being monitored yet")