diff --git a/TODO b/TODO deleted file mode 100644 index 02bc8f30..00000000 --- a/TODO +++ /dev/null @@ -1,2 +0,0 @@ -TODOS - - Make troi silent by default, enabled current level of messages only for CLI use. Coming in next PR. diff --git a/tests/test_filters.py b/tests/test_filters.py index 1bc117f0..042ffe5f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -215,7 +215,7 @@ def test_hated_recordings_filter(self, mock_requests): filter_element = troi.filters.HatedRecordingsFilterElement() filter_element.set_sources(feedback_lookup) - received = filter_element.generate() + received = filter_element.generate(quiet=True) expected = [ Recording(mbid="53969964-673a-4407-9396-3087be9245f6", listenbrainz={"score": 1}), Recording(mbid="8e7a9ff8-c31d-4ac0-a01d-20a7fcc28c8f", listenbrainz={"score": 0}) diff --git a/troi/__init__.py b/troi/__init__.py index 8957847c..6a0b6f40 100644 --- a/troi/__init__.py +++ b/troi/__init__.py @@ -1,10 +1,16 @@ -from abc import ABC, abstractmethod import logging import random +from abc import ABC, abstractmethod from typing import Dict from troi.utils import recursively_update_dict +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +_handler = logging.StreamHandler() +_handler.setFormatter(logging.Formatter("%(message)s")) +logger.addHandler(_handler) + DEVELOPMENT_SERVER_URL = "https://datasets.listenbrainz.org" # Number of recordings that each patch should aim to generate @@ -18,7 +24,6 @@ class Element(ABC): def __init__(self, patch=None): self.sources = [] - self.logger = logging.getLogger(type(self).__name__) self.patch = patch def set_patch_object(self, patch): @@ -38,19 +43,6 @@ def local_storage(self): return self.patch.local_storage - - def log(self, msg): - ''' - Log a message with the info log level, which is the default for troi. - ''' - self.logger.info(msg) - - def debug(self, msg): - ''' - Log a message with debug log level. These messages will only be shown when debugging is enabled. - ''' - self.logger.debug(msg) - def set_sources(self, sources): """ Set the source elements for this element. @@ -59,7 +51,7 @@ def set_sources(self, sources): """ if not isinstance(sources, list): - sources = [ sources ] + sources = [sources] self.sources = sources @@ -73,9 +65,7 @@ def set_sources(self, sources): break if not matched: - raise RuntimeError("Element %s cannot accept any of %s as input." % - (type(self).__name__, self.inputs())) - + raise RuntimeError("Element %s cannot accept any of %s as input." % (type(self).__name__, self.inputs())) def check(self): """ @@ -89,8 +79,7 @@ def check(self): for source in self.sources: source.check() - - def generate(self): + def generate(self, quiet): """ Generate output from the pipeline. This should be called on the last element in the pipeline and no where else. At the root @@ -101,14 +90,14 @@ def generate(self): source_lists = [] if self.sources: for source in self.sources: - result = source.generate() + result = source.generate(quiet) if result is None: return None if len(self.inputs()) > 0 and \ len(result) > 0 and type(result[0]) not in self.inputs() and \ len(source.outputs()) > 0: - raise RuntimeError("Element %s was expected to output %s, but actually output %s" % + raise RuntimeError("Element %s was expected to output %s, but actually output %s" % (type(source).__name__, source.outputs()[0], type(result[0]))) source_lists.append(result) @@ -117,10 +106,11 @@ def generate(self): if items is None: return None - if len(items) > 0 and type(items[0]) == Playlist: - print(" %-50s %d items" % (type(self).__name__[:49], len(items[0].recordings or []))) - else: - print(" %-50s %d items" % (type(self).__name__[:49], len(items or []))) + if not quiet: + if len(items) > 0 and type(items[0]) == Playlist: + logger.info(" %-50s %d items" % (type(self).__name__[:49], len(items[0].recordings or []))) + else: + logger.info(" %-50s %d items" % (type(self).__name__[:49], len(items or []))) return items @@ -160,8 +150,7 @@ def read(self, source_data_list): read data from the pipeline, it calls read() on the last element in the pipeline and this casues the while pipeline to generate result. If the initializers of other objects in the pipeline are updated, - calling read() again will generate the set new. Passing True for - debug should print helpful debug statements about its progress. + calling read() again will generate the set new. Note: This function should not be called directly by the user. ''' @@ -180,6 +169,7 @@ class Entity(ABC): of an artist or the listenbrainz dict might contain the BPM for a track. How exactly these dicts will be organized is TDB. """ + def __init__(self, ranking=None, musicbrainz=None, listenbrainz=None, acousticbrainz=None): self.name = None self.mbid = None @@ -215,6 +205,7 @@ class Area(Entity): """ The class that represents an area. """ + def __init__(self, id=id, name=None): Entity.__init__(self) self.name = name @@ -228,8 +219,15 @@ class Artist(Entity): """ The class that represents an artist. """ - def __init__(self, name=None, mbids=None, artist_credit_id=None, ranking=None, - musicbrainz=None, listenbrainz=None, acousticbrainz=None): + + def __init__(self, + name=None, + mbids=None, + artist_credit_id=None, + ranking=None, + musicbrainz=None, + listenbrainz=None, + acousticbrainz=None): Entity.__init__(self, ranking=ranking, musicbrainz=musicbrainz, listenbrainz=listenbrainz, acousticbrainz=acousticbrainz) self.name = name self.artist_credit_id = artist_credit_id @@ -248,8 +246,8 @@ class Release(Entity): """ The class that represents a release. """ - def __init__(self, name=None, mbid=None, artist=None, ranking=None, - musicbrainz=None, listenbrainz=None, acousticbrainz=None): + + def __init__(self, name=None, mbid=None, artist=None, ranking=None, musicbrainz=None, listenbrainz=None, acousticbrainz=None): Entity.__init__(self, ranking=ranking, musicbrainz=musicbrainz, listenbrainz=listenbrainz, acousticbrainz=acousticbrainz) self.artist = artist self.name = name @@ -263,10 +261,22 @@ class Recording(Entity): """ The class that represents a recording. """ - def __init__(self, name=None, mbid=None, msid=None, duration=None, artist=None, release=None, - ranking=None, year=None, spotify_id=None, musicbrainz=None, listenbrainz=None, acousticbrainz=None): + + def __init__(self, + name=None, + mbid=None, + msid=None, + duration=None, + artist=None, + release=None, + ranking=None, + year=None, + spotify_id=None, + musicbrainz=None, + listenbrainz=None, + acousticbrainz=None): Entity.__init__(self, ranking=ranking, musicbrainz=musicbrainz, listenbrainz=listenbrainz, acousticbrainz=acousticbrainz) - self.duration = duration # track duration in ms + self.duration = duration # track duration in ms self.artist = artist self.release = release self.name = name @@ -289,8 +299,20 @@ class Playlist(Entity): and that filename is the suggested filename that this playlist should be saved as, if the user asked to do that and didn't provide a different filename. """ - def __init__(self, name=None, mbid=None, filename=None, recordings=None, description=None, ranking=None, - year=None, musicbrainz=None, listenbrainz=None, acousticbrainz=None, patch_slug=None, user_name=None, + + def __init__(self, + name=None, + mbid=None, + filename=None, + recordings=None, + description=None, + ranking=None, + year=None, + musicbrainz=None, + listenbrainz=None, + acousticbrainz=None, + patch_slug=None, + user_name=None, additional_metadata=None): Entity.__init__(self, ranking=ranking, musicbrainz=musicbrainz, listenbrainz=listenbrainz, acousticbrainz=acousticbrainz) self.name = name @@ -323,6 +345,7 @@ class User(Entity): """ The class that represents a ListenBrainz user. """ + def __init__(self, user_name=None, user_id=None): Entity.__init__(self) self.user_name = user_name diff --git a/troi/cli.py b/troi/cli.py index 2b3feb27..ede5a1cb 100755 --- a/troi/cli.py +++ b/troi/cli.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 - +import logging import sys + import click +from troi.logging_utils import set_log_level from troi.utils import discover_patches from troi.core import list_patches, patch_info, convert_patch_to_command from troi.content_resolver.cli import cli as resolver_cli, db_file_check, output_playlist @@ -11,6 +13,8 @@ from troi.content_resolver.playlist import read_jspf_playlist from troi.local.periodic_jams_local import PeriodicJamsLocal +logger = logging.getLogger(__name__) + try: sys.path.insert(1, ".") import config @@ -30,8 +34,7 @@ def cli(): @cli.command(context_settings=dict(ignore_unknown_options=True, )) @click.argument('patch', type=str) -@click.option('--debug/--no-debug', help="Turn on/off debug statements") -@click.option('--print', '-p', 'echo', help="Show the generated playlist", required=False, is_flag=True) +@click.option('--quiet', '-q', 'quiet', help="Do no print out anything", required=False, is_flag=True) @click.option('--save', '-s', help="Save the generated playlist", required=False, is_flag=True) @click.option( '--token', @@ -59,26 +62,28 @@ def cli(): required=False, multiple=True) @click.argument('args', nargs=-1, type=click.UNPROCESSED) -def playlist(patch, debug, echo, save, token, upload, args, created_for, name, desc, min_recordings, spotify_user_id, spotify_token, +def playlist(patch, quiet, save, token, upload, args, created_for, name, desc, min_recordings, spotify_user_id, spotify_token, spotify_url): """ Generate a global MBID based playlist using a patch """ + + set_log_level(quiet) patchname = patch patches = discover_patches() if patchname not in patches: - print("Cannot load patch '%s'. Use the list command to get a list of available patches." % patchname, file=sys.stderr) + logger.info("Cannot load patch '%s'. Use the list command to get a list of available patches." % patchname) return None patch_args = { - "echo": echo, "save": save, "token": token, "created_for": created_for, "upload": upload, "name": name, "desc": desc, - "min_recordings": min_recordings + "min_recordings": min_recordings, + "quiet": quiet, } if spotify_token: patch_args["spotify"] = { @@ -97,15 +102,15 @@ def playlist(patch, debug, echo, save, token, upload, args, created_for, name, d patch_args.update(context.forward(cmd)) # Create the actual patch, finally - patch = patches[patchname](patch_args, debug) + patch = patches[patchname](patch_args) ret = patch.generate_playlist() user_feedback = patch.user_feedback() if len(user_feedback) > 0: - print("User feedback:") + logger.info("User feedback:") for feedback in user_feedback: - print(f" * {feedback}") - print() + logger.info(f" * {feedback}") + logger.info("") sys.exit(0 if ret else -1) @@ -119,7 +124,7 @@ def list_patches_cli(): @cli.command(name="info") @click.argument("patch", nargs=1) -def info(patch): +def info_cmd(patch): """Get info for a given patch""" ret = patch_info(patch) sys.exit(0 if ret else -1) @@ -132,13 +137,15 @@ def info(patch): @click.option('-m', '--save-to-m3u', required=False, help="save to specified m3u playlist") @click.option('-j', '--save-to-jspf', required=False, help="save to specified JSPF playlist") @click.option('-y', '--dont-ask', required=False, is_flag=True, help="save playlist without asking user") +@click.option('-q', '--quiet', 'quiet', help="Do no print out anything", required=False, is_flag=True) @click.argument('jspf_playlist') -def resolve(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_jspf, dont_ask, jspf_playlist): +def resolve(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_jspf, dont_ask, quiet, jspf_playlist): """ Resolve a global JSPF playlist with MusicBrainz MBIDs to files in the local collection""" + set_log_level(quiet) db_file = db_file_check(db_file) - db = SubsonicDatabase(db_file, config) + db = SubsonicDatabase(db_file, config, quiet) db.open() - lbrl = ListenBrainzRadioLocal() + lbrl = ListenBrainzRadioLocal(quiet) playlist = read_jspf_playlist(jspf_playlist) lbrl.resolve_playlist(threshold, playlist) output_playlist(db, playlist, upload_to_subsonic, save_to_m3u, save_to_jspf, dont_ask) @@ -151,14 +158,16 @@ def resolve(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_jspf, d @click.option('-m', '--save-to-m3u', required=False, help="save to specified m3u playlist") @click.option('-j', '--save-to-jspf', required=False, help="save to specified JSPF playlist") @click.option('-y', '--dont-ask', required=False, is_flag=True, help="save playlist without asking user") +@click.option('-q', '--quiet', 'quiet', help="Do no print out anything", required=False, is_flag=True) @click.argument('mode') @click.argument('prompt') -def lb_radio(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_jspf, dont_ask, mode, prompt): +def lb_radio(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_jspf, dont_ask, quiet, mode, prompt): """Use LB Radio to create a playlist from a prompt, using a local music collection""" + set_log_level(quiet) db_file = db_file_check(db_file) - db = SubsonicDatabase(db_file, config) + db = SubsonicDatabase(db_file, config, quiet) db.open() - r = ListenBrainzRadioLocal() + r = ListenBrainzRadioLocal(quiet) playlist = r.generate(mode, prompt, threshold) try: _ = playlist.playlists[0].recordings[0] @@ -176,14 +185,16 @@ def lb_radio(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_jspf, @click.option('-m', '--save-to-m3u', required=False, help="save to specified m3u playlist") @click.option('-j', '--save-to-jspf', required=False, help="save to specified JSPF playlist") @click.option('-y', '--dont-ask', required=False, is_flag=True, help="save playlist without asking user") +@click.option('-q', '--quiet', 'quiet', help="Do no print out anything", required=False, is_flag=True) @click.argument('user_name') -def periodic_jams(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_jspf, dont_ask, user_name): +def periodic_jams(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_jspf, dont_ask, quiet, user_name): "Generate a weekly jams playlist for your local collection" + set_log_level(quiet) db_file = db_file_check(db_file) - db = SubsonicDatabase(db_file, config) + db = SubsonicDatabase(db_file, config, quiet) db.open() - pj = PeriodicJamsLocal(user_name, threshold) + pj = PeriodicJamsLocal(user_name, threshold, quiet) playlist = pj.generate() try: _ = playlist.playlists[0].recordings[0] @@ -194,7 +205,7 @@ def periodic_jams(db_file, threshold, upload_to_subsonic, save_to_m3u, save_to_j output_playlist(db, playlist, upload_to_subsonic, save_to_m3u, save_to_jspf, dont_ask) -@cli.command(context_settings=dict(ignore_unknown_options=True, )) +@cli.command(context_settings=dict(ignore_unknown_options=True,)) @click.argument('args', nargs=-1, type=click.UNPROCESSED) def test(args): """Run unit tests""" diff --git a/troi/content_resolver/cli.py b/troi/content_resolver/cli.py index 3748487e..bb612b65 100755 --- a/troi/content_resolver/cli.py +++ b/troi/content_resolver/cli.py @@ -1,14 +1,11 @@ #!/usr/bin/env python3 - -import os -import sys +import logging import sys import click -from troi.content_resolver.content_resolver import ContentResolver +from troi.logging_utils import set_log_level from troi.content_resolver.database import Database -from troi.content_resolver.model.recording import FileIdType from troi.content_resolver.subsonic import SubsonicDatabase from troi.content_resolver.metadata_lookup import MetadataLookup from troi.content_resolver.utils import ask_yes_no_question @@ -16,17 +13,16 @@ from troi.content_resolver.duplicates import FindDuplicates from troi.content_resolver.playlist import write_m3u_playlist, write_jspf_playlist from troi.content_resolver.unresolved_recording import UnresolvedRecordingTracker -from troi.playlist import PLAYLIST_TRACK_EXTENSION_URI + +logger = logging.getLogger(__name__) # TODO: Soon we will need a better configuration file, so we can get rid of this hack try: sys.path.insert(1, ".") import config except ImportError as err: - print(err) config = None - DEFAULT_CHUNKSIZE = 100 @@ -34,7 +30,7 @@ def output_playlist(db, playlist, upload_to_subsonic, save_to_m3u, save_to_jspf, try: recording = playlist.playlists[0].recordings[0] except (KeyError, IndexError): - print("Cannot save empty playlist.") + logger.error("Cannot save empty playlist.") return if upload_to_subsonic and config: @@ -42,11 +38,11 @@ def output_playlist(db, playlist, upload_to_subsonic, save_to_m3u, save_to_jspf, try: _ = recording.musicbrainz["subsonic_id"] except KeyError: - print("Playlist does not appear to contain subsonic ids. Can't upload to subsonic.") + logger.info("Playlist does not appear to contain subsonic ids. Can't upload to subsonic.") return if dont_ask or ask_yes_no_question("Upload via subsonic? (Y/n)"): - print("uploading playlist") + logger.info("uploading playlist") db.upload_playlist(playlist) return @@ -54,22 +50,22 @@ def output_playlist(db, playlist, upload_to_subsonic, save_to_m3u, save_to_jspf, try: _ = recording.musicbrainz["filename"] except KeyError: - print("Playlist does not appear to contain file paths. Can't write a local playlist.") + logger.error("Playlist does not appear to contain file paths. Can't write a local playlist.") return if save_to_m3u: if dont_ask or ask_yes_no_question(f"Save to '{save_to_m3u}'? (Y/n)"): - print("saving playlist") + logger.info("saving playlist") write_m3u_playlist(save_to_m3u, playlist) return if save_to_jspf: if dont_ask or ask_yes_no_question(f"Save to '{save_to_jspf}'? (Y/n)"): - print("saving playlist") + logger.info("saving playlist") write_jspf_playlist(save_to_jspf, playlist) return - print("Playlist displayed, but not saved. Use -j, -m or -u options to save/upload playlists.") + logger.info("Playlist displayed, but not saved. Use -j, -m or -u options to save/upload playlists.") def db_file_check(db_file): @@ -77,11 +73,11 @@ def db_file_check(db_file): if not db_file: if not config: - print("Database file not specified with -d (--db_file) argument. Consider adding it to config.py for ease of use.") + logger.info("Database file not specified with -d (--db_file) argument. Consider adding it to config.py for ease of use.") sys.exit(-1) if not config.DATABASE_FILE: - print("config.py found, but DATABASE_FILE is empty. Please add it or use -d option to specify it.") + logger.info("config.py found, but DATABASE_FILE is empty. Please add it or use -d option to specify it.") sys.exit(-1) return config.DATABASE_FILE @@ -105,10 +101,12 @@ def cli(): @click.command() @click.option("-d", "--db_file", help="Database file for the local collection", required=False, is_flag=False) -def create(db_file): +@click.option('--quiet', '-q', 'quiet', help="Do no print out anything", required=False, is_flag=True) +def create(db_file, quiet): """Create a new database to track a music collection""" + set_log_level(quiet) db_file = db_file_check(db_file) - db = Database(db_file) + db = Database(db_file, quiet) db.create() @@ -116,13 +114,15 @@ def create(db_file): @click.option("-d", "--db_file", help="Database file for the local collection", required=False, is_flag=False) @click.option('-c', '--chunksize', default=DEFAULT_CHUNKSIZE, help="Number of files to add/update at once") @click.option("-f", "--force", required=False, is_flag=True, default=False, help="Force scanning, ignoring any cache") +@click.option('-q', '--quiet', 'quiet', help="Do no print out anything", required=False, is_flag=True) @click.argument('music_dirs', nargs=-1, type=click.Path()) -def scan(db_file, music_dirs, chunksize=DEFAULT_CHUNKSIZE, force=False): +def scan(db_file, music_dirs, quiet, chunksize=DEFAULT_CHUNKSIZE, force=False): """Scan one or more directories and their subdirectories for music files to add to the collection. If no path is passed, check for MUSIC_DIRECTORIES in config instead. """ + set_log_level(quiet) db_file = db_file_check(db_file) - db = Database(db_file) + db = Database(db_file, quiet) db.open() if not music_dirs: music_dirs = music_directories_from_config() @@ -136,35 +136,41 @@ def scan(db_file, music_dirs, chunksize=DEFAULT_CHUNKSIZE, force=False): @click.command() @click.option("-d", "--db_file", help="Database file for the local collection", required=False, is_flag=False) @click.option("-r", "--remove", help="Without this flag, no files are removed.", required=False, is_flag=True, default=True) -def cleanup(db_file, remove): +@click.option('-q', '--quiet', 'quiet', help="Do no print out anything", required=False, is_flag=True) +def cleanup(db_file, remove, quiet): """Perform a database cleanup. Check that files exist and if they don't remove from the index""" + set_log_level(quiet) db_file = db_file_check(db_file) - db = Database(db_file) + db = Database(db_file, quiet) db.open() db.database_cleanup(remove) @click.command() @click.option("-d", "--db_file", help="Database file for the local collection", required=False, is_flag=False) -def metadata(db_file): +@click.option('-q', '--quiet', 'quiet', help="Do no print out anything", required=False, is_flag=True) +def metadata(db_file, quiet): """Lookup metadata (popularity and tags) for recordings""" + set_log_level(quiet) db_file = db_file_check(db_file) - db = Database(db_file) + db = Database(db_file, quiet) db.open() - lookup = MetadataLookup() + lookup = MetadataLookup(quiet) lookup.lookup() - print("\nThese top tags describe your collection:") + logger.info("\nThese top tags describe your collection:") tt = TopTags() tt.print_top_tags_tightly(100) @click.command() @click.option("-d", "--db_file", help="Database file for the local collection", required=False, is_flag=False) -def subsonic(db_file): +@click.option('-q', '--quiet', 'quiet', help="Do no print out anything", required=False, is_flag=True) +def subsonic(db_file, quiet): """Scan a remote subsonic music collection""" + set_log_level(quiet) db_file = db_file_check(db_file) - db = SubsonicDatabase(db_file, config) + db = SubsonicDatabase(db_file, config, quiet) db.open() db.sync() @@ -174,8 +180,9 @@ def subsonic(db_file): @click.argument('count', required=False, default=250) def top_tags(db_file, count): "Display the top most used tags in the music collection. Useful for writing LB Radio tag prompts" + set_log_level(False) db_file = db_file_check(db_file) - db = Database(db_file) + db = Database(db_file, False) db.open() tt = TopTags() tt.print_top_tags_tightly(count) @@ -183,25 +190,30 @@ def top_tags(db_file, count): @click.command() @click.option("-d", "--db_file", help="Database file for the local collection", required=False, is_flag=False) -@click.option('-e', '--exclude-different-release', help="Exclude duplicates that appear on different releases", - required=False, default=False, is_flag=True) +@click.option('-e', + '--exclude-different-release', + help="Exclude duplicates that appear on different releases", + required=False, + default=False, + is_flag=True) @click.option('-v', '--verbose', help="Display extra info about found files", required=False, default=False, is_flag=True) def duplicates(db_file, exclude_different_release, verbose): "Print all the tracks in the DB that are duplicated as per recording_mbid" + set_log_level(False) db_file = db_file_check(db_file) - db = Database(db_file) + db = Database(db_file, False) db.open() fd = FindDuplicates(db) fd.print_duplicate_recordings(exclude_different_release, verbose) - @click.command() @click.option("-d", "--db_file", help="Database file for the local collection", required=False, is_flag=False) def unresolved(db_file): "Show the top unresolved releases" + set_log_level(False) db_file = db_file_check(db_file) - db = Database(db_file) + db = Database(db_file, False) db.open() urt = UnresolvedRecordingTracker() releases = urt.get_releases() diff --git a/troi/content_resolver/content_resolver.py b/troi/content_resolver/content_resolver.py index e31f94f6..481851bc 100755 --- a/troi/content_resolver/content_resolver.py +++ b/troi/content_resolver/content_resolver.py @@ -1,19 +1,12 @@ -import os -import datetime -import sys -from uuid import UUID +import logging -import peewee - -from troi.content_resolver.model.database import db, setup_db from troi.content_resolver.model.recording import Recording, FileIdType from troi.content_resolver.unresolved_recording import UnresolvedRecordingTracker from troi.content_resolver.fuzzy_index import FuzzyIndex from lb_matching_tools.cleaner import MetadataCleaner -from troi.content_resolver.playlist import read_jspf_playlist from troi.content_resolver.utils import bcolors -from troi.playlist import PlaylistElement -from troi import Playlist + +logger = logging.getLogger(__name__) class ContentResolver: @@ -21,8 +14,9 @@ class ContentResolver: Scan a given path and enter/update the metadata in the search index ''' - def __init__(self): + def __init__(self, quiet): self.fuzzy_index = None + self.quiet = quiet def get_artist_recording_metadata(self): """ @@ -183,15 +177,15 @@ def resolve_playlist(self, match_threshold, playlist): # Build index based on recording.id rec_index = {r["id"]: r for r in local_recordings} - print(" %-40s %-40s %-40s" % ("RECORDING", "RELEASE", "ARTIST")) + logger.info(" %-40s %-40s %-40s" % ("RECORDING", "RELEASE", "ARTIST")) unresolved_recordings = [] target_recordings = playlist.playlists[0].recordings resolved = 0 failed = 0 for i, artist_recording in enumerate(artist_recording_data): if i not in hit_index: - print(bcolors.FAIL + "FAIL " + bcolors.ENDC + " %-40s %-40s %-40s" % (artist_recording["recording_name"][:39], "", - artist_recording["artist_name"][:39])) + logger.info(bcolors.FAIL + "FAIL " + bcolors.ENDC + " %-40s %-40s %-40s" % (artist_recording["recording_name"][:39], "", + artist_recording["artist_name"][:39])) unresolved_recordings.append(artist_recording["recording_mbid"]) failed += 1 continue @@ -207,17 +201,20 @@ def resolve_playlist(self, match_threshold, playlist): if local_recording["duration"] is not None: target.duration = local_recording["duration"] - print(bcolors.OKGREEN + ("%-5s" % hit["method"]) + bcolors.ENDC + - " %-40s %-40s %-40s" % (artist_recording["recording_name"][:39], "", - artist_recording["artist_name"][:39])) - print(" %-40s %-40s %-40s" % (local_recording["recording_name"][:39], - local_recording["release_name"][:39], - local_recording["artist_name"][:39])) + if not self.quiet: + logger.info(bcolors.OKGREEN + ("%-5s" % hit["method"]) + bcolors.ENDC + + " %-40s %-40s %-40s" % (artist_recording["recording_name"][:39], "", + artist_recording["artist_name"][:39])) + logger.info(" %-40s %-40s %-40s" % (local_recording["recording_name"][:39], + local_recording["release_name"][:39], + local_recording["artist_name"][:39])) resolved += 1 if resolved == 0: - print("Sorry, but no tracks could be resolved, no playlist generated.") + logger.info("Sorry, but no tracks could be resolved, no playlist generated.") return [] - print(f'\n{resolved} recordings resolved, {failed} not resolved.') + if not self.quiet: + logger.info(f'\n{resolved} recordings resolved, {failed} not resolved.') + return playlist diff --git a/troi/content_resolver/database.py b/troi/content_resolver/database.py index e4d2a8aa..9cb026ca 100755 --- a/troi/content_resolver/database.py +++ b/troi/content_resolver/database.py @@ -1,3 +1,4 @@ +import logging from abc import abstractmethod from collections import namedtuple from enum import IntEnum @@ -6,11 +7,9 @@ from mutagen import MutagenError from pathlib import Path import sys -from time import time from types import SimpleNamespace from uuid import UUID -from unidecode import unidecode import peewee from tqdm import tqdm @@ -23,6 +22,8 @@ from troi.content_resolver.utils import existing_dirs +logger = logging.getLogger(__name__) + SUPPORTED_FORMATS = ( flac, m4a, @@ -97,10 +98,11 @@ class Database: Keep a database with metadata for a collection of local music files. ''' - def __init__(self, db_file): + def __init__(self, db_file, quiet): self.db_file = db_file self.fuzzy_index = None self.forced_scan = False + self.quiet = quiet def create(self): """ @@ -121,7 +123,7 @@ def create(self): Directory, )) except Exception as e: - print("Failed to create db file %r: %s" % (self.db_file, e)) + logger.error("Failed to create db file %r: %s" % (self.db_file, e)) def open(self): """ @@ -131,7 +133,7 @@ def open(self): setup_db(self.db_file) db.connect() except peewee.OperationalError: - print("Cannot open database index file: '%s'" % self.db_file) + logger.error("Cannot open database index file: '%s'" % self.db_file) sys.exit(-1) def close(self): @@ -143,14 +145,14 @@ def scan(self, music_dirs, chunksize=100, force=False): Scan music directories and add tracks to sqlite. """ if not music_dirs: - print("No directory to scan") + logger.error("No directory to scan") return self.forced_scan = force self.music_dirs = tuple(sorted(set(existing_dirs(music_dirs)))) if not self.music_dirs: - print("No valid directories to scan") + logger.error("No valid directories to scan") return self.chunksize = chunksize @@ -159,17 +161,23 @@ def scan(self, music_dirs, chunksize=100, force=False): self.counters = ScanCounters() self.skip_dirs = set() - print("Check collection size...") - print("Counting candidates in %s ..." % ", ".join(self.music_dirs)) + if not self.quiet: + logger.info("Check collection size...") + logger.info("Counting candidates in %s ..." % ", ".join(self.music_dirs)) self.traverse(dry_run=True) - print(self.counters.dry_run_stats()) + if not self.quiet: + logger.info(self.counters.dry_run_stats()) - with tqdm(total=self.counters.audio_files) as self.progress_bar: - print("Scanning ...") + if not self.quiet: + with tqdm(total=self.counters.audio_files) as self.progress_bar: + logger.info("Scanning ...") + self.traverse() + else: self.traverse() self.close() - print(self.counters.stats()) + if not self.quiet: + logger.info(self.counters.stats()) def traverse(self, dry_run=False): """ @@ -237,7 +245,7 @@ def dir_has_changed(self, dir_path): if directory is None or directory.mtime != mtime: return mtime except Exception as e: - print("Can't stat dir %r: %s" % (dir_path, e)) + logger.error("Can't stat dir %r: %s" % (dir_path, e)) return False def read_metadata(self, file_path, mtime): @@ -345,7 +353,8 @@ def update_status(self, statusdata): Update status counter and display matching progress """ self.counters.status[statusdata.status] += 1 - self.progress_bar.write(self.fmtdetails(statusdata)) + if not self.quiet: + self.progress_bar.write(self.fmtdetails(statusdata)) def add(self, file_path, audio_file_count): """ @@ -355,7 +364,8 @@ def add(self, file_path, audio_file_count): """ # update the progress bar - self.progress_bar.update(1) + if not self.quiet: + self.progress_bar.update(1) self.counters.total += 1 @@ -419,28 +429,28 @@ def database_cleanup(self, dry_run): PathId(d.dir_path, d.id) for d in Directory.select(Directory.dir_path, Directory.id) if not os.path.isdir(d.dir_path)) if not recordings and not directories: - print("No cleanup needed.") + logger.error("No cleanup needed.") return for elem in sorted(recordings + directories): - print("RM %s" % elem.path) + logger.info("RM %s" % elem.path) - print("%d recordings and %d directory entries to remove from database" % (len(recordings), len(directories))) + logger.info("%d recordings and %d directory entries to remove from database" % (len(recordings), len(directories))) if not dry_run: with db.atomic(): ids = tuple(r.id for r in recordings) query = Recording.delete().where(Recording.id.in_(ids)) count = query.execute() - print("%d recordings removed" % count) + logger.info("%d recordings removed" % count) ids = tuple(d.id for d in directories) query = Directory.delete().where(Directory.id.in_(ids)) count = query.execute() - print("%d directory entries removed" % count) - print("Vacuuming database...") + logger.info("%d directory entries removed" % count) + logger.info("Vacuuming database...") db.execute_sql('VACUUM') - print("Done.") + logger.info("Done.") else: - print("Use command cleanup --remove to actually remove those.") + logger.info("Use command cleanup --remove to actually remove those.") def metadata_sanity_check(self, include_subsonic=False): """ @@ -455,16 +465,15 @@ def metadata_sanity_check(self, include_subsonic=False): Recording.file_id_type).alias('count')).where(Recording.file_id_type == FileIdType.SUBSONIC_ID)[0].count if num_metadata == 0: - print("sanity check: You have not downloaded metadata for your collection. Run the metadata command.") + logger.info("sanity check: You have not downloaded metadata for your collection. Run the metadata command.") elif num_metadata < num_recordings // 2: - print("sanity check: Only %d of your %d recordings have metadata information available. Run the metdata command." % - (num_metadata, num_recordings)) + logger.info("sanity check: Only %d of your %d recordings have metadata information available." + " Run the metdata command." % (num_metadata, num_recordings)) if include_subsonic: if num_subsonic == 0 and include_subsonic: - print( - "sanity check: You have not matched your collection against the collection in subsonic. Run the subsonic command." - ) + logger.info("sanity check: You have not matched your collection against the collection in subsonic." + " Run the subsonic command.") elif num_subsonic < num_recordings // 2: - print("sanity check: Only %d of your %d recordings have subsonic matches. Run the subsonic command." % - (num_subsonic, num_recordings)) + logger.info("sanity check: Only %d of your %d recordings have subsonic matches." + " Run the subsonic command." % (num_subsonic, num_recordings)) diff --git a/troi/content_resolver/duplicates.py b/troi/content_resolver/duplicates.py index dc113716..990fc7c3 100755 --- a/troi/content_resolver/duplicates.py +++ b/troi/content_resolver/duplicates.py @@ -1,18 +1,12 @@ +import logging import os import json -from collections import defaultdict -import datetime import hashlib import mutagen -import sys - -import peewee -import requests from troi.content_resolver.model.database import db -from troi.content_resolver.model.recording import Recording, RecordingMetadata -from troi.recording_search_service import RecordingSearchByTagService -from troi.splitter import plist + +logger = logging.getLogger(__name__) class FindDuplicates: @@ -73,16 +67,16 @@ def indent(n, s=''): return ' ' * (4 * n) + str(s) def print_error(e): - print(indent(2, "error: %s" % e)) + logger.info(indent(2, "error: %s" % e)) def print_info(title, content): - print(indent(2, "%s: %s" % (title, content))) + logger.info(indent(2, "%s: %s" % (title, content))) for dup in self.get_duplicate_recordings(include_different_releases): recordings_count += 1 - print("%d duplicates of '%s' by '%s'" % (dup[5], dup[0], dup[2])) + logger.info("%d duplicates of '%s' by '%s'" % (dup[5], dup[0], dup[2])) for file_id in dup[4]: - print(indent(1, file_id)) + logger.info(indent(1, file_id)) if verbose: error = False try: @@ -107,8 +101,7 @@ def print_info(title, content): print_error(e) total += 1 - print() + logger.info("") - print() - print("%d recordings had a total of %d duplicates." % - (recordings_count, total)) + logger.info("") + logger.info("%d recordings had a total of %d duplicates." % (recordings_count, total)) diff --git a/troi/content_resolver/formats/flac.py b/troi/content_resolver/formats/flac.py index ea09d6e8..f787851b 100755 --- a/troi/content_resolver/formats/flac.py +++ b/troi/content_resolver/formats/flac.py @@ -1,4 +1,3 @@ -import mutagen import mutagen.flac from troi.content_resolver.formats.tag_utils import get_tag_value, extract_track_number diff --git a/troi/content_resolver/formats/m4a.py b/troi/content_resolver/formats/m4a.py index f6a1e545..cafa6e3f 100755 --- a/troi/content_resolver/formats/m4a.py +++ b/troi/content_resolver/formats/m4a.py @@ -1,4 +1,3 @@ -import mutagen import mutagen.mp4 from troi.content_resolver.formats.tag_utils import get_tag_value, extract_track_number diff --git a/troi/content_resolver/formats/mp3.py b/troi/content_resolver/formats/mp3.py index 8041ac2b..c9766e42 100755 --- a/troi/content_resolver/formats/mp3.py +++ b/troi/content_resolver/formats/mp3.py @@ -1,4 +1,3 @@ -import mutagen import mutagen.mp3 from troi.content_resolver.formats.tag_utils import get_tag_value, extract_track_number diff --git a/troi/content_resolver/formats/ogg_vorbis.py b/troi/content_resolver/formats/ogg_vorbis.py index 84424e99..f8e0bba0 100755 --- a/troi/content_resolver/formats/ogg_vorbis.py +++ b/troi/content_resolver/formats/ogg_vorbis.py @@ -1,4 +1,3 @@ -import mutagen import mutagen.oggvorbis from troi.content_resolver.formats.tag_utils import get_tag_value, extract_track_number diff --git a/troi/content_resolver/formats/wma.py b/troi/content_resolver/formats/wma.py index 2a0bdef8..03b2eddd 100755 --- a/troi/content_resolver/formats/wma.py +++ b/troi/content_resolver/formats/wma.py @@ -1,4 +1,3 @@ -import mutagen import mutagen.asf from troi.content_resolver.formats.tag_utils import get_tag_value, extract_track_number diff --git a/troi/content_resolver/lb_radio.py b/troi/content_resolver/lb_radio.py index 195c62db..68dc91f5 100755 --- a/troi/content_resolver/lb_radio.py +++ b/troi/content_resolver/lb_radio.py @@ -1,24 +1,24 @@ -import datetime -import os +import logging from troi import Playlist from troi.playlist import PlaylistElement -from troi.patches.lb_radio_classes.tag import LBRadioTagRecordingElement from troi.patches.lb_radio import LBRadioPatch -from troi.splitter import plist from troi.content_resolver.tag_search import LocalRecordingSearchByTagService from troi.content_resolver.artist_search import LocalRecordingSearchByArtistService -from troi.content_resolver.model.database import db -from troi.content_resolver.model.recording import FileIdType from troi.content_resolver.content_resolver import ContentResolver +logger = logging.getLogger(__name__) + class ListenBrainzRadioLocal: ''' Generate local playlists against a music collection available via subsonic. ''' + def __init__(self, quiet): + self.quiet = quiet + def generate(self, mode, prompt, match_threshold): """ Generate a playlist given the mode and prompt. Optional match_threshold, a value from @@ -28,7 +28,7 @@ def generate(self, mode, prompt, match_threshold): Returns a troi playlist object. """ - patch = LBRadioPatch({"mode": mode, "prompt": prompt, "echo": True, "debug": True, "min_recordings": 1}) + patch = LBRadioPatch({"mode": mode, "prompt": prompt, "quiet": self.quiet, "min_recordings": 1}) patch.register_service(LocalRecordingSearchByTagService()) patch.register_service(LocalRecordingSearchByArtistService()) @@ -36,10 +36,10 @@ def generate(self, mode, prompt, match_threshold): try: playlist = patch.generate_playlist() except RuntimeError as err: - print(f"LB Radio generation failed: {err}") + logger.info(f"LB Radio generation failed: {err}") return None - if playlist == None: + if playlist is None: return playlist # Resolve any tracks that have not been resolved to a subsonic_id or a local file @@ -62,7 +62,7 @@ def resolve_playlist(self, match_threshold, playlist): return # Use the content resolver to resolve the recordings in situ - cr = ContentResolver() + cr = ContentResolver(self.quiet) pe = PlaylistElement() pe.playlists = [ Playlist(recordings=recordings) ] cr.resolve_playlist(match_threshold, pe) diff --git a/troi/content_resolver/metadata_lookup.py b/troi/content_resolver/metadata_lookup.py index cde5ef97..af0a4831 100755 --- a/troi/content_resolver/metadata_lookup.py +++ b/troi/content_resolver/metadata_lookup.py @@ -1,9 +1,7 @@ -import os +import logging from collections import defaultdict, namedtuple import datetime -import sys -import peewee import requests from tqdm import tqdm @@ -11,6 +9,8 @@ from troi.content_resolver.model.recording import Recording, RecordingMetadata from troi.content_resolver.model.tag import RecordingTag +logger = logging.getLogger(__name__) + RecordingRow = namedtuple('RecordingRow', ('id', 'mbid', 'metadata_id')) @@ -22,6 +22,9 @@ class MetadataLookup: BATCH_SIZE = 1000 + def __init__(self, quiet): + self.quiet = quiet + def lookup(self): """ Iterate over all recordings in the database and call lookup_chunk for chunks of recordings. @@ -38,10 +41,16 @@ def lookup(self): for row in cursor.fetchall() ) - print("[ %d recordings to lookup ]" % len(recordings)) + logger.info("[ %d recordings to lookup ]" % len(recordings)) offset = 0 - with tqdm(total=len(recordings)) as self.pbar: + + if not self.quiet: + with tqdm(total=len(recordings)) as self.pbar: + while offset <= len(recordings): + self.process_recordings(recordings[offset:offset+self.BATCH_SIZE]) + offset += self.BATCH_SIZE + else: while offset <= len(recordings): self.process_recordings(recordings[offset:offset+self.BATCH_SIZE]) offset += self.BATCH_SIZE @@ -60,7 +69,7 @@ def process_recordings(self, recordings): r = requests.post("https://labs.api.listenbrainz.org/bulk-tag-lookup/json", json=args) if r.status_code != 200: - print("Fail: %d %s" % (r.status_code, r.text)) + logger.info("Fail: %d %s" % (r.status_code, r.text)) return False recording_pop = {} @@ -72,7 +81,8 @@ def process_recordings(self, recordings): recording_tags[mbid][row["source"]].append(row["tag"]) tags.add(row["tag"]) - self.pbar.update(len(recordings)) + if not self.quiet: + self.pbar.update(len(recordings)) with db.atomic(): diff --git a/troi/content_resolver/subsonic.py b/troi/content_resolver/subsonic.py index 03ea773c..aac6903a 100755 --- a/troi/content_resolver/subsonic.py +++ b/troi/content_resolver/subsonic.py @@ -1,7 +1,5 @@ import datetime -import os -import sys -from uuid import UUID +import logging import peewee from tqdm import tqdm @@ -12,6 +10,8 @@ from troi.content_resolver.utils import bcolors from troi.content_resolver.py_sonic_fix import FixedConnection +logger = logging.getLogger(__name__) + class SubsonicDatabase(Database): ''' @@ -21,9 +21,10 @@ class SubsonicDatabase(Database): # Determined by the number of albums we can fetch in one go BATCH_SIZE = 500 - def __init__(self, index_dir, config): + def __init__(self, index_dir, config, quiet): self.config = config - Database.__init__(self, index_dir) + Database.__init__(self, index_dir, quiet) + self.quiet = quiet def sync(self): """ @@ -36,17 +37,17 @@ def sync(self): self.error = 0 self.run_sync() - - print("Checked %s albums:" % self.total) - print(" %5d albums matched" % self.matched) - print(" %5d recordings with errors" % self.error) + + logger.info("Checked %s albums:" % self.total) + logger.info(" %5d albums matched" % self.matched) + logger.info(" %5d recordings with errors" % self.error) def connect(self): if not self.config: - print("Missing credentials to connect to subsonic") + logger.error("Missing credentials to connect to subsonic") return None - print("[ connect to subsonic ]") + logger.info("[ connect to subsonic ]") return FixedConnection( self.config.SUBSONIC_HOST, @@ -66,7 +67,7 @@ def run_sync(self): cursor = db.connection().cursor() - print("[ load albums ]") + logger.info("[ load albums ]") album_ids = set() albums = [] offset = 0 @@ -80,9 +81,10 @@ def run_sync(self): if album_count < self.BATCH_SIZE: break - print("[ loaded %d albums ]" % len(album_ids)) + logger.info("[ loaded %d albums ]" % len(album_ids)) - pbar = tqdm(total=len(album_ids)) + if not self.quiet: + pbar = tqdm(total=len(album_ids)) recordings = [] # cross reference subsonic artist id to artitst_mbid @@ -98,8 +100,9 @@ def run_sync(self): try: album_mbid = album_info2["albumInfo"]["musicBrainzId"] except KeyError: - pbar.write(bcolors.FAIL + "FAIL " + bcolors.ENDC + "subsonic album '%s' by '%s' has no MBID" % - (album["name"], album["artist"])) + if not self.quiet: + pbar.write(bcolors.FAIL + "FAIL " + bcolors.ENDC + "subsonic album '%s' by '%s' has no MBID" % + (album["name"], album["artist"])) self.error += 1 continue @@ -118,16 +121,13 @@ def run_sync(self): try: artist_id_index[artist_id] = artist["artistInfo2"]["musicBrainzId"] except KeyError: - pbar.write(bcolors.FAIL + "FAIL " + bcolors.ENDC + "recording '%s' by '%s' has no artist MBID" % - (album["name"], album["artist"])) - pbar.write("Consider retagging this file with Picard! ( https://picard.musicbrainz.org )") + if not self.quiet: + pbar.write(bcolors.FAIL + "FAIL " + bcolors.ENDC + "recording '%s' by '%s' has no artist MBID" % + (album["name"], album["artist"])) + pbar.write("Consider retagging this file with Picard! ( https://picard.musicbrainz.org )") self.error += 1 continue -# if "musicBrainzId" not in song: -# song_details = conn.getSong(song["id"]) -# ic(song_details) - self.add_subsonic({ "artist_name": song["artist"], "release_name": song["album"], @@ -142,11 +142,13 @@ def run_sync(self): "mtime": datetime.datetime.now() }) - pbar.write(bcolors.OKGREEN + "OK " + bcolors.ENDC + "album %-50s %-50s" % - (album["name"][:49], album["artist"][:49])) + if not self.quiet: + pbar.write(bcolors.OKGREEN + "OK " + bcolors.ENDC + "album %-50s %-50s" % + (album["name"][:49], album["artist"][:49])) self.matched += 1 self.total += 1 - pbar.update(1) + if not self.quiet: + pbar.update(1) if len(recordings) >= self.BATCH_SIZE: self.update_recordings(recordings) diff --git a/troi/content_resolver/top_tags.py b/troi/content_resolver/top_tags.py index 7bdc9a0f..e72f67b9 100755 --- a/troi/content_resolver/top_tags.py +++ b/troi/content_resolver/top_tags.py @@ -1,15 +1,8 @@ -import os -from collections import defaultdict -import datetime -import sys - -import peewee -import requests +import logging from troi.content_resolver.model.database import db -from troi.content_resolver.model.recording import Recording, RecordingMetadata -from troi.recording_search_service import RecordingSearchByTagService -from troi.splitter import plist + +logger = logging.getLogger(__name__) class TopTags: @@ -44,11 +37,11 @@ def print_top_tags(self, limit=50): top_tags = self.get_top_tags(limit) for tt in top_tags: - print("%-40s %d" % (tt["tag"], tt["count"])) - print() + logger.info("%-40s %d" % (tt["tag"], tt["count"])) + logger.info("") def print_top_tags_tightly(self, limit=250): top_tags = self.get_top_tags(limit) - print("; ".join(["%s %s" % (tt["tag"], tt["count"]) for tt in top_tags])) + logger.info("; ".join(["%s %s" % (tt["tag"], tt["count"]) for tt in top_tags])) diff --git a/troi/content_resolver/unresolved_recording.py b/troi/content_resolver/unresolved_recording.py index e686bee6..967e50ef 100755 --- a/troi/content_resolver/unresolved_recording.py +++ b/troi/content_resolver/unresolved_recording.py @@ -1,13 +1,14 @@ -from collections import defaultdict import datetime -from math import ceil +import logging +from collections import defaultdict from operator import itemgetter -import requests +from time import sleep -import peewee +import requests from troi.content_resolver.model.database import db -from troi.content_resolver.model.unresolved_recording import UnresolvedRecording + +logger = logging.getLogger(__name__) class UnresolvedRecordingTracker: @@ -100,7 +101,7 @@ def get_releases(self): while True: r = requests.get("https://api.listenbrainz.org/1/metadata/recording", params=params) if r.status_code != 200: - print("Failed to fetch metadata for recordings: ", r.text) + logger.info("Failed to fetch metadata for recordings: ", r.text) return [] if r.status_code == 429: @@ -141,12 +142,12 @@ def get_releases(self): def print_releases(self, releases): """ Neatly print all the release/recordings returned from the get_releases function """ - print("%-60s %-50s" % ("RELEASE", "ARTIST")) + info("%-60s %-50s" % ("RELEASE", "ARTIST")) for release in releases: - print("%-60s %-50s" % (release["release_name"][:59], release["artist_name"][:49])) + info("%-60s %-50s" % (release["release_name"][:59], release["artist_name"][:49])) for rec in release["recordings"]: - print(" %-57s %d lookups" % (rec["recording_name"][:56], rec["lookup_count"])) - print() + info(" %-57s %d lookups" % (rec["recording_name"][:56], rec["lookup_count"])) + info() def cleanup(self): """ diff --git a/troi/content_resolver/utils.py b/troi/content_resolver/utils.py index 8f4e31e5..b18e5225 100755 --- a/troi/content_resolver/utils.py +++ b/troi/content_resolver/utils.py @@ -1,9 +1,12 @@ +import logging import os from troi.splitter import plist from troi import Recording as TroiRecording from troi.content_resolver.model.recording import FileIdType +logger = logging.getLogger(__name__) + def ask_yes_no_question(prompt): @@ -18,7 +21,7 @@ def ask_yes_no_question(prompt): elif resp == 'n': return False else: - print("eh? try again.") + logging.info("eh? try again.") def select_recordings_on_popularity(recordings, begin_percent, end_percent, num_recordings): diff --git a/troi/core.py b/troi/core.py index 61254a01..67ddb918 100755 --- a/troi/core.py +++ b/troi/core.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 +import logging import sys -from typing import Dict import click import troi import troi.playlist import troi.utils -from troi.patch import Patch +logger = logging.getLogger(__name__) + def list_patches(): """ @@ -17,7 +18,7 @@ def list_patches(): """ patches = troi.utils.discover_patches() - print("Available patches:") + logger.info("Available patches:") size = max([len(k) for k in patches]) for slug in sorted(patches or []): patch = patches[slug] @@ -33,7 +34,7 @@ def patch_info(patch): patches = troi.utils.discover_patches() if patch not in patches: - print("Cannot load patch '%s'. Use the list command to get a list of available patches." % patch, file=sys.stderr) + logger.error("Cannot load patch '%s'. Use the list command to get a list of available patches." % patch) sys.exit(1) apatch = patches[patch] diff --git a/troi/filters.py b/troi/filters.py index ee1085c2..dc804cd6 100644 --- a/troi/filters.py +++ b/troi/filters.py @@ -48,7 +48,6 @@ def read(self, inputs): results = [] for r in recordings: if not r.artist or not r.artist.artist_credit_id: - self.debug("recording %s has not artist credit id" % (r.mbid)) continue if self.include: @@ -157,7 +156,7 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, inputs): recordings = inputs[0] output = [] seen = set() @@ -183,12 +182,11 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, inputs): recordings = inputs[0] index = {} for rec in recordings: if rec.name is None or rec.name == "" or rec.artist is None or rec.artist.name is None or rec.artist.name == "": - self.debug("Recording %s has insufficient metadata, removing" % rec.mbid) continue k = rec.name + rec.artist.name @@ -213,7 +211,7 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, inputs): recordings = inputs[0] output = [] last_mbid = None @@ -238,14 +236,11 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, input): recordings = inputs[0] output = [] for rec in recordings: - if rec.name is None or (rec.artist and rec.artist.name is None) or rec.mbid is None: - if debug: - print(f"recording {rec.mbid} has no metadata, filtering") - else: + if rec.name is not None and (rec.artist and rec.artist.name is not None) and rec.mbid is not None: output.append(rec) return output @@ -278,7 +273,7 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, inputs): recordings = inputs[0] @@ -322,7 +317,7 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, inputs): recordings = inputs[0] @@ -360,7 +355,7 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, inputs): recordings = inputs[0] @@ -399,7 +394,7 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, inputs): recordings = inputs[0] @@ -432,7 +427,7 @@ def inputs(): def outputs(): return [Recording] - def read(self, inputs, debug=False): + def read(self, inputs): results = [] for r in inputs[0]: score = r.listenbrainz.get("score", 0) diff --git a/troi/internal/top_new_recordings_you_listened_to_for_year.py b/troi/internal/top_new_recordings_you_listened_to_for_year.py index 96effe2d..2374dcf8 100755 --- a/troi/internal/top_new_recordings_you_listened_to_for_year.py +++ b/troi/internal/top_new_recordings_you_listened_to_for_year.py @@ -34,8 +34,8 @@ class TopTracksYouListenedToPatch(troi.patch.Patch): This is a review playlist that we hope will give insights the music released during the year.

""" - def __init__(self, debug=False, max_num_recordings=50): - troi.patch.Patch.__init__(self, debug) + def __init__(self, max_num_recordings=50): + troi.patch.Patch.__init__(self) self.max_num_recordings = max_num_recordings @staticmethod diff --git a/troi/internal/top_recordings_for_year.py b/troi/internal/top_recordings_for_year.py index 55c18277..6eacbd2e 100755 --- a/troi/internal/top_recordings_for_year.py +++ b/troi/internal/top_recordings_for_year.py @@ -36,8 +36,8 @@ class TopTracksYearPatch(troi.patch.Patch):

""" - def __init__(self, debug=False, max_num_recordings=50): - troi.patch.Patch.__init__(self, debug) + def __init__(self, max_num_recordings=50): + troi.patch.Patch.__init__(self) self.max_num_recordings = max_num_recordings @staticmethod diff --git a/troi/internal/top_sitewide_recordings_for_year.py b/troi/internal/top_sitewide_recordings_for_year.py index 14481bd3..d19a54f8 100755 --- a/troi/internal/top_sitewide_recordings_for_year.py +++ b/troi/internal/top_sitewide_recordings_for_year.py @@ -26,8 +26,8 @@ class TopSitewideRecordingsPatch(troi.patch.Patch):

""" - def __init__(self, debug=False, max_num_recordings=50): - troi.patch.Patch.__init__(self, debug) + def __init__(self, max_num_recordings=50): + troi.patch.Patch.__init__(self) self.max_num_recordings = max_num_recordings @staticmethod diff --git a/troi/internal/yim_patch_runner.py b/troi/internal/yim_patch_runner.py index d4f5f32a..e11a62a6 100755 --- a/troi/internal/yim_patch_runner.py +++ b/troi/internal/yim_patch_runner.py @@ -1,11 +1,15 @@ +import logging + import click import troi -from troi import Element, Artist, Recording, Playlist, PipelineError +from troi import Element, Playlist from troi.listenbrainz.yim_user import YIMUserListElement from troi.loops import ForLoopElement from troi.playlist import PlaylistElement +logger = logging.getLogger(__name__) + @click.group() def cli(): @@ -36,12 +40,12 @@ def read(self, inputs): slug = inputs[0][0].patch_slug metadata = {"algorithm_metadata": {"source_patch": slug}} with open("%s-playlists.json" % slug, "w") as f: - print("YIMSubmitter:") + logger.info("YIMSubmitter:") for playlist in inputs[0]: if len(playlist.recordings) == 0: continue - print(" ", playlist.patch_slug, playlist.user_name) + logger.info(" ", playlist.patch_slug, playlist.user_name) f.write("%s\n" % playlist.user_name) f.write("%s\n" % playlist.mbid) @@ -53,7 +57,7 @@ def read(self, inputs): playlist_element.save(track_count=5, file_obj=f) f.write("\n") - print("") + logger.info("") return None @@ -64,8 +68,8 @@ class YIMRunnerPatch(troi.patch.Patch): """ - def __init__(self, debug=False): - troi.patch.Patch.__init__(self, debug) + def __init__(self): + troi.patch.Patch.__init__(self) @staticmethod @cli.command(no_args_is_help=True) @@ -101,9 +105,9 @@ def description(): def create(self, inputs): patch_slugs = [ slug for slug in inputs["patch_slugs"].split(",") ] - print("Running the following patches:") + logger.info("Running the following patches:") for slug in patch_slugs: - print(" %s" % slug) + logger.info(" %s" % slug) u = YIMUserListElement() diff --git a/troi/listenbrainz/stats.py b/troi/listenbrainz/stats.py index 90c62d60..68bc404d 100644 --- a/troi/listenbrainz/stats.py +++ b/troi/listenbrainz/stats.py @@ -1,7 +1,11 @@ +import logging + from troi import Element, Artist, Release, Recording import liblistenbrainz import liblistenbrainz.errors +logger = logging.getLogger(__name__) + class UserArtistsElement(Element): ''' @@ -26,7 +30,7 @@ def __init__(self, user_name, count=25, offset=0, time_range='all_time', auth_to def outputs(self): return [Artist] - def read(self, inputs = []): + def read(self, inputs=[]): artist_list = [] artists = self.client.get_user_artists(self.user_name, self.count, self.offset, self.time_range) @@ -59,7 +63,7 @@ def __init__(self, user_name, count=25, offset=0, time_range='all_time', auth_to def outputs(self): return [Release] - def read(self, inputs = []): + def read(self, inputs=[]): release_list = [] releases = self.client.get_user_releases(self.user_name, self.count, self.offset, self.time_range) @@ -91,12 +95,12 @@ def __init__(self, user_name, count=25, offset=0, time_range='all_time'): def outputs(self): return [Recording] - def read(self, inputs = []): + def read(self, inputs=[]): recording_list = [] try: recordings = self.client.get_user_recordings(self.user_name, self.count, self.offset, self.time_range) except liblistenbrainz.errors.ListenBrainzAPIException as err: - print("Cannot fetch recording stats for user %s" % self.user_name) + logger.info("Cannot fetch recording stats for user %s" % self.user_name) return [] if recordings is None or "recordings" not in recordings['payload']: diff --git a/troi/local/periodic_jams_local.py b/troi/local/periodic_jams_local.py index eba4eaee..5e668dac 100755 --- a/troi/local/periodic_jams_local.py +++ b/troi/local/periodic_jams_local.py @@ -1,25 +1,30 @@ +import logging + from troi.content_resolver.lb_radio import ListenBrainzRadioLocal from troi.patches.periodic_jams_local import PeriodicJamsLocalPatch +logger = logging.getLogger(__name__) + class PeriodicJamsLocal(ListenBrainzRadioLocal): ''' Generate local playlists against a music collection available via subsonic. ''' - def __init__(self, user_name, match_threshold): - ListenBrainzRadioLocal.__init__(self) + def __init__(self, user_name, match_threshold, quiet): + ListenBrainzRadioLocal.__init__(self, quiet) self.user_name = user_name self.match_threshold = match_threshold + self.quiet = quiet def generate(self): """ Generate a periodic jams playlist """ - + patch = PeriodicJamsLocalPatch({ "user_name": self.user_name, - "echo": True, + "quiet": self.quiet, "debug": True, "min_recordings": 1 }) @@ -28,11 +33,11 @@ def generate(self): try: playlist = patch.generate_playlist() except RuntimeError as err: - print(f"LB Radio generation failed: {err}") + logger.info(f"LB Radio generation failed: {err}") return None - if playlist == None: - print("Your prompt generated an empty playlist.") + if playlist is None: + logger.info("Your prompt generated an empty playlist.") return {"playlist": {"track": []}} # Resolve any tracks that have not been resolved to a subsonic_id or a local file diff --git a/troi/local/recording_resolver.py b/troi/local/recording_resolver.py index 16601007..693427d9 100644 --- a/troi/local/recording_resolver.py +++ b/troi/local/recording_resolver.py @@ -12,12 +12,12 @@ class RecordingResolverElement(Element): name set and resolves them to a local collection by using the ContentResolver class """ - def __init__(self, match_threshold): + def __init__(self, match_threshold, quiet): """ Match threshold: The value from 0 to 1.0 on how sure a match must be to be accepted. """ Element.__init__(self) self.match_threshold = match_threshold - self.resolve = ContentResolver() + self.resolve = ContentResolver(quiet) @staticmethod def inputs(): diff --git a/troi/logging_utils.py b/troi/logging_utils.py new file mode 100644 index 00000000..fe91d4a4 --- /dev/null +++ b/troi/logging_utils.py @@ -0,0 +1,7 @@ +import logging + + +def set_log_level(quiet): + logger = logging.getLogger("troi") + level = logging.ERROR if quiet else logging.INFO + logger.setLevel(level) diff --git a/troi/loops.py b/troi/loops.py index dd3116ad..5a185f61 100644 --- a/troi/loops.py +++ b/troi/loops.py @@ -1,11 +1,12 @@ -from collections import defaultdict +import logging from copy import copy -import sys import troi -from troi import PipelineError, Element, User +from troi import PipelineError, User from troi.utils import discover_patches +logger = logging.getLogger(__name__) + class ForLoopElement(troi.Element): ''' @@ -47,17 +48,17 @@ def read(self, inputs): self.patch_args["created_for"] = user.user_name try: - print("generate %s for %s" % (patch_slug, user.user_name)) + logger.info("generate %s for %s" % (patch_slug, user.user_name)) playlist = troi.playlist.PlaylistElement() playlist.set_sources(pipeline) playlist.generate() if self.patch_args["min_recordings"] is not None and \ len(playlist.playlists[0].recordings) < self.patch_args["min_recordings"]: - print("Playlist does not have at least %d recordings, not submitting.\n" % self.patch_args["min_recordings"]) + logger.info("Playlist does not have at least %d recordings, not submitting.\n" % self.patch_args["min_recordings"]) continue - if self.patch_args["echo"]: + if not self.patch_args["quiet"]: playlist.print() playlist.add_metadata({"algorithm_metadata": {"source_patch": patch_slug}}) if self.patch_args["upload"]: @@ -70,13 +71,13 @@ def read(self, inputs): else: playlist.submit(self.patch_args["token"]) except troi.PipelineError as err: - print("Failed to submit playlist: %s, continuing..." % err, file=sys.stderr) + logger.error("Failed to submit playlist: %s, continuing..." % err) continue outputs.append(playlist.playlists[0]) - print() + logger.info("") except troi.PipelineError as err: - print("Failed to generate playlist: %s" % err, file=sys.stderr) + logger.info("Failed to generate playlist: %s" % err) raise # Return None if you want to stop processing this pipeline diff --git a/troi/musicbrainz/recording_lookup.py b/troi/musicbrainz/recording_lookup.py index bae5fd8b..12dfcba2 100644 --- a/troi/musicbrainz/recording_lookup.py +++ b/troi/musicbrainz/recording_lookup.py @@ -45,8 +45,6 @@ def read(self, inputs): if len(data) == 0: return inputs[0] - self.debug("- debug %d recordings" % len(recordings)) - while True: r = requests.post(self.SERVER_URL % len(recordings), json=data) if r.status_code == 429: @@ -60,7 +58,6 @@ def read(self, inputs): try: rows = ujson.loads(r.text) - self.debug("- debug %d rows in response" % len(rows)) except ValueError as err: raise PipelineError("Cannot fetch recordings from ListenBrainz: " + str(err)) @@ -75,9 +72,7 @@ def read(self, inputs): if row["recording_mbid"] is None: continue except KeyError: - if self.skip_not_found: - self.debug("- debug recording MBID %s not found, skipping." % r.mbid) - else: + if not self.skip_not_found: output.append(r) continue diff --git a/troi/musicbrainz/year_lookup.py b/troi/musicbrainz/year_lookup.py index 7ce8a2ec..b74395d2 100644 --- a/troi/musicbrainz/year_lookup.py +++ b/troi/musicbrainz/year_lookup.py @@ -64,9 +64,7 @@ def read(self, inputs): try: r.year = mbid_index[r.artist.name + r.name] except KeyError: - if self.skip_not_found: - self.debug("recording (%s %s) not found, skipping." % (r.artist.name, r.name)) - else: + if not self.skip_not_found: output.append(r) continue diff --git a/troi/patch.py b/troi/patch.py index 6431df8d..48b9ed3d 100755 --- a/troi/patch.py +++ b/troi/patch.py @@ -1,12 +1,14 @@ import logging + import troi from abc import ABC, abstractmethod +from troi.logging_utils import set_log_level from troi.recording_search_service import RecordingSearchByTagService, RecordingSearchByArtistService -default_patch_args = dict(debug=False, - echo=True, - save=False, +logger = logging.getLogger(__name__) + +default_patch_args = dict(save=False, token=None, upload=False, args=None, @@ -14,19 +16,15 @@ name=None, desc=None, min_recordings=10, - spotify=None) + spotify=None, + quiet=False) class Patch(ABC): - def __init__(self, args, debug=False): + def __init__(self, args): + self.quiet = False self.args = args - if debug: - level = logging.DEBUG - else: - level = logging.INFO - logging.basicConfig(level=level) - self.logger = logging.getLogger(type(self).__name__) # Dict used for local storage self.local_storage = {} @@ -40,20 +38,6 @@ def __init__(self, args, debug=False): self.register_service(RecordingSearchByTagService()) self.register_service(RecordingSearchByArtistService()) - def log(self, msg): - ''' - Log a message with the info log level, which is the default for troi. - - :param msg: The message to log. - ''' - self.logger.info(msg) - - def debug(self, msg): - ''' - Log a message with debug log level. These messages will only be shown when debugging is enabled. - ''' - self.logger.debug(msg) - @staticmethod def inputs(): """ @@ -144,8 +128,7 @@ def generate_playlist(self): The args parameter is a dict and may containt the following keys: - * debug: Print debug information or not - * print: This option causes the generated playlist to be printed to stdout. + * quiet: Do not print out anything * save: The save option causes the generated playlist to be saved to disk. * token: Auth token to use when using the LB API. Required for submitting playlists to the server. See https://listenbrainz.org/profile to get your user token. * upload: Whether or not to submit the finished playlist to the LB server. Token must be set for this to work. @@ -160,10 +143,11 @@ def generate_playlist(self): """ try: + set_log_level(self.patch_args.get("quiet", False)) playlist = troi.playlist.PlaylistElement() playlist.set_sources(self.pipeline) - print("Troi playlist generation starting...") - result = playlist.generate() + logger.info("Troi playlist generation starting...") + result = playlist.generate(self.quiet) name = self.patch_args["name"] if name: @@ -173,53 +157,49 @@ def generate_playlist(self): if desc: playlist.playlists[0].descripton = desc - print("done.") + logger.info("done.") except troi.PipelineError as err: - print("Failed to generate playlist: %s" % err, file=sys.stderr) + logging.error("Failed to generate playlist: %s" % err) return None upload = self.patch_args["upload"] token = self.patch_args["token"] spotify = self.patch_args["spotify"] if upload and not token and not spotify: - print("In order to upload a playlist, you must provide an auth token. Use option --token.") + logger.info("In order to upload a playlist, you must provide an auth token. Use option --token.") return None min_recordings = self.patch_args["min_recordings"] if min_recordings is not None and \ (len(playlist.playlists) == 0 or len(playlist.playlists[0].recordings) < min_recordings): - print("Playlist does not have at least %d recordings, stopping." % min_recordings) + logger.info("Playlist does not have at least %d recordings, stopping." % min_recordings) return None save = self.patch_args["save"] if result is not None and spotify and upload: for url, _ in playlist.submit_to_spotify(spotify["user_id"], spotify["token"], spotify["is_public"], spotify["is_collaborative"], spotify.get("existing_urls", [])): - print("Submitted playlist to spotify: %s" % url) + logger.info("Submitted playlist to spotify: %s" % url) created_for = self.patch_args["created_for"] if result is not None and token and upload: for url, _ in playlist.submit(token, created_for): - print("Submitted playlist: %s" % url) + logger.info("Submitted playlist: %s" % url) if result is not None and save: playlist.save() - print("playlist saved.") + logger.info("playlist saved.") - echo = self.patch_args["echo"] - if result is not None and (echo or not token): - print() + if not self.quiet and result is not None: + logger.info("") playlist.print() - if not echo and not save and not token: - if result is None: - print("Patch executed successfully.") - elif len(playlist.playlists) == 0: - print("No playlists were generated. :(") - elif len(playlist.playlists) == 1: - print("A playlist with %d tracks was generated." % len(playlist.playlists[0].recordings)) - else: - print("%d playlists were generated." % len(playlist.playlists)) + if len(playlist.playlists) == 0: + logger.info("No playlists were generated. :(") + elif len(playlist.playlists) == 1: + logger.info("A playlist with %d tracks was generated." % len(playlist.playlists[0].recordings)) + else: + logger.info("%d playlists were generated." % len(playlist.playlists)) return playlist diff --git a/troi/patches/area_random_recordings.py b/troi/patches/area_random_recordings.py index d472f9ec..d25a9fc2 100755 --- a/troi/patches/area_random_recordings.py +++ b/troi/patches/area_random_recordings.py @@ -13,8 +13,8 @@ class AreaRandomRecordingsPatch(troi.patch.Patch): SERVER_URL = DEVELOPMENT_SERVER_URL + "/area-random-recordings/json" - def __init__(self, args, debug=False): - super().__init__(args, debug) + def __init__(self, args): + super().__init__(args) @staticmethod def inputs(): diff --git a/troi/patches/lb_radio.py b/troi/patches/lb_radio.py index 52b4dedb..e98f2b00 100755 --- a/troi/patches/lb_radio.py +++ b/troi/patches/lb_radio.py @@ -31,12 +31,12 @@ class LBRadioPatch(troi.patch.Patch): # If the user specifies no time_range, default to this one DEFAULT_TIME_RANGE = "month" - def __init__(self, args, debug=False): + def __init__(self, args): self.artist_mbids = [] self.mode = None # Remember, the create function for this class will be called in the super() init. - super().__init__(args, debug) + super().__init__(args) @staticmethod def inputs(): diff --git a/troi/patches/periodic_jams.py b/troi/patches/periodic_jams.py index b82cf351..b9973d59 100755 --- a/troi/patches/periodic_jams.py +++ b/troi/patches/periodic_jams.py @@ -46,8 +46,8 @@ class PeriodicJamsPatch(troi.patch.Patch): JAM_TYPES = ("daily-jams", "weekly-jams", "weekly-exploration") - def __init__(self, args, debug=False): - super().__init__(args, debug) + def __init__(self, args): + super().__init__(args) @staticmethod def inputs(): @@ -104,8 +104,7 @@ def create(self, inputs): recs = troi.listenbrainz.recs.UserRecordingRecommendationsElement(user_name, "raw", - count=1000, - auth_token=inputs.get("token")) + count=1000) recent_listens_lookup = troi.listenbrainz.listens.RecentListensTimestampLookup(user_name, days=2, diff --git a/troi/patches/periodic_jams_local.py b/troi/patches/periodic_jams_local.py index 94f64605..652250c5 100755 --- a/troi/patches/periodic_jams_local.py +++ b/troi/patches/periodic_jams_local.py @@ -18,8 +18,8 @@ class PeriodicJamsLocalPatch(troi.patch.Patch): """ """ - def __init__(self, args, debug=False): - super().__init__(args, debug) + def __init__(self, args): + super().__init__(args) @staticmethod def inputs(): @@ -69,10 +69,10 @@ def create(self, inputs): recs_lookup = troi.musicbrainz.recording_lookup.RecordingLookupElement() recs_lookup.set_sources(feedback_lookup) - resolve = RecordingResolverElement(.8) + resolve = RecordingResolverElement(.8, self.quiet) resolve.set_sources(recs_lookup) - pl_maker = PlaylistMakerElement(name="Local Periodic Jams for %s" % (user_name), + pl_maker = PlaylistMakerElement(name="Weekly Jams for %s" % (user_name), desc="test playlist!", patch_slug="periodic-jams", max_num_recordings=50, diff --git a/troi/patches/playlist_from_mbids.py b/troi/patches/playlist_from_mbids.py index 9d451646..0deac2ce 100755 --- a/troi/patches/playlist_from_mbids.py +++ b/troi/patches/playlist_from_mbids.py @@ -9,8 +9,8 @@ class PlaylistFromMBIDsPatch(troi.patch.Patch): """ """ - def __init__(self, args, debug=False): - troi.patch.Patch.__init__(self, args, debug) + def __init__(self, args): + troi.patch.Patch.__init__(self, args) @staticmethod def inputs(): diff --git a/troi/patches/recs_to_playlist.py b/troi/patches/recs_to_playlist.py index 70b1dcef..70709f92 100755 --- a/troi/patches/recs_to_playlist.py +++ b/troi/patches/recs_to_playlist.py @@ -58,8 +58,8 @@ class RecommendationsToPlaylistPatch(troi.patch.Patch): See below for description """ - def __init__(self, args, debug=False): - troi.patch.Patch.__init__(self, args, debug) + def __init__(self, args): + troi.patch.Patch.__init__(self, args) @staticmethod def inputs(): diff --git a/troi/patches/top_discoveries_for_year.py b/troi/patches/top_discoveries_for_year.py index 27e3208b..a9aafba7 100755 --- a/troi/patches/top_discoveries_for_year.py +++ b/troi/patches/top_discoveries_for_year.py @@ -25,8 +25,8 @@ class TopDiscoveries(troi.patch.Patch):

""" - def __init__(self, args, debug=False): - troi.patch.Patch.__init__(self, args, debug) + def __init__(self, args): + troi.patch.Patch.__init__(self, args) @staticmethod def inputs(): diff --git a/troi/patches/top_missed_recordings_for_year.py b/troi/patches/top_missed_recordings_for_year.py index 49de5ff4..69290f29 100755 --- a/troi/patches/top_missed_recordings_for_year.py +++ b/troi/patches/top_missed_recordings_for_year.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime import psycopg2 @@ -8,10 +9,10 @@ from troi.playlist import PlaylistMakerElement from troi.musicbrainz.recording_lookup import RecordingLookupElement +logger = logging.getLogger(__name__) class MissedRecordingsElement(Element): - """ This element looks up top tracks for 3 users minus the tracks of one given user to form the core of the missed tracks playlist for yim. """ @@ -80,8 +81,8 @@ class TopMissedTracksPatch(troi.patch.Patch):

""" - def __init__(self, args, debug=False, max_num_recordings=50): - troi.patch.Patch.__init__(self, args, debug) + def __init__(self, args, max_num_recordings=50): + troi.patch.Patch.__init__(self, args) self.max_num_recordings = max_num_recordings @staticmethod @@ -132,8 +133,7 @@ def create(self, inputs): mb_curs.execute("""SELECT id, musicbrainz_id FROM "user" WHERE id IN %s""", (tuple(similar_user_ids),)) your_peeps = ", ".join([ f'{r["musicbrainz_id"]}' for r in mb_curs.fetchall() ]) - print(your_peeps) - + logger.info(your_peeps) missed = MissedRecordingsElement(user_id, similar_user_ids, lb_db_connect_str) diff --git a/troi/patches/weekly_flashback_jams.py b/troi/patches/weekly_flashback_jams.py index 983386b3..2c9a55fc 100755 --- a/troi/patches/weekly_flashback_jams.py +++ b/troi/patches/weekly_flashback_jams.py @@ -63,8 +63,8 @@ class WeeklyFlashbackJams(troi.patch.Patch): See below for description """ - def __init__(self, args, debug=False): - troi.patch.Patch.__init__(self, args, debug) + def __init__(self, args): + troi.patch.Patch.__init__(self, args) @staticmethod def inputs(): @@ -95,7 +95,6 @@ def description(): def create(self, inputs): user_name = inputs['user_name'] type = inputs['type'] - print(type) if type not in ("top", "similar", "raw"): raise PipelineError("type must be either 'top', 'similar' or 'raw'") diff --git a/troi/patches/world_trip.py b/troi/patches/world_trip.py index e046025c..8908f9c7 100755 --- a/troi/patches/world_trip.py +++ b/troi/patches/world_trip.py @@ -1,3 +1,4 @@ +import logging from collections import defaultdict from urllib.parse import quote @@ -7,6 +8,8 @@ import troi.patch from troi import Element, Artist, Recording, Playlist, PipelineError, DEVELOPMENT_SERVER_URL +logger = logging.getLogger(__name__) + def recording_from_row(row): if row['recording_mbid'] is None: @@ -69,15 +72,13 @@ def read(self, inputs): names = continents.keys() raise RuntimeError(f"Cannot find continent {self.continent}. Must be one of {names}") - print("Fetch tracks from countries:") + logger.info("Fetch tracks from countries:") if self.latitude: continent = sorted(continents[self.continent], key=lambda c: c['latlng'][0], reverse=True) else: continent = sorted(continents[self.continent], key=lambda c: c['latlng'][1]) for i, country in enumerate(continent): - self.debug(" %s" % country["name"]) - r = requests.get("http://musicbrainz.org/ws/2/area?query=%s&fmt=json" % country['name']) if r.status_code != 200: raise PipelineError("Cannot fetch country code from MusicBrainz. HTTP code %s" % r.status_code) @@ -102,9 +103,9 @@ def read(self, inputs): try: recordings.append(country["recordings"][i]) except KeyError: - print("Found no tracks for %s" % country["name"]) + logger.error("Found no tracks for %s" % country["name"]) except IndexError: - print("Found too few tracks for %s" % country["name"]) + logger.error("Found too few tracks for %s" % country["name"]) return recordings @@ -122,8 +123,8 @@ class WorldTripPatch(troi.patch.Patch):

""" - def __init__(self, debug=False): - troi.patch.Patch.__init__(self, debug) + def __init__(self): + troi.patch.Patch.__init__(self) @staticmethod def inputs(): diff --git a/troi/playlist.py b/troi/playlist.py index afa074fa..6781b77f 100755 --- a/troi/playlist.py +++ b/troi/playlist.py @@ -1,3 +1,4 @@ +import logging from collections import defaultdict import json @@ -9,7 +10,8 @@ from troi.operations import is_homogeneous from troi.print_recording import PrintRecordingList from troi.tools.spotify_lookup import submit_to_spotify -from troi.playlist import PrintRecordingList + +logger = logging.getLogger(__name__) LISTENBRAINZ_SERVER_URL = "https://listenbrainz.org" LISTENBRAINZ_API_URL = "https://api.listenbrainz.org" @@ -28,6 +30,7 @@ # Artist.mbids is totatlly stupid (see ^^). We need [artists] with "join_phrase" in musicbrainz hash. # All this for the next PR. + def _serialize_to_jspf(playlist, created_for=None, track_count=None): """ Serialize a playlist to JSPF. @@ -179,7 +182,7 @@ def read(self, inputs): for input in inputs: if len(input) == 0: - print("No recordings or playlists generated to save.") + logger.info("No recordings or playlists generated to save.") continue if isinstance(input[0], Recording): @@ -199,18 +202,18 @@ def print(self): """Prints the resultant playlists, one after another.""" if not self.playlists: - print("[no playlist(s) generated yet]") + logger.error("[no playlist(s) generated yet]") return for i, playlist in enumerate(self.playlists): if playlist.name: - print("playlist: '%s'" % playlist.name) + logger.info("playlist: '%s'" % playlist.name) else: - print("playlist: %d" % i) + logger.info("playlist: %d" % i) for recording in playlist.recordings: if not recording: - print("[invalid Recording]") + logger.info("[invalid Recording]") continue self.print_recording.print(recording) @@ -257,7 +260,7 @@ def submit(self, token, created_for=None): if len(playlist.recordings) == 0: continue - print("submit %d tracks" % len(playlist.recordings)) + info("submit %d tracks" % len(playlist.recordings)) if playlist.patch_slug is not None: playlist.add_metadata({"algorithm_metadata": {"source_patch": playlist.patch_slug}}) r = requests.post(LISTENBRAINZ_PLAYLIST_CREATE_URL, diff --git a/troi/print_recording.py b/troi/print_recording.py index ee360682..d0ce412c 100755 --- a/troi/print_recording.py +++ b/troi/print_recording.py @@ -1,6 +1,9 @@ import datetime +import logging + from troi import Recording, Playlist, PipelineError +logger = logging.getLogger(__name__) class PrintRecordingList: """ @@ -67,39 +70,40 @@ def _print_recording(self, recording, year=False, popularity=False, listen_count else: rec_name = recording.name rec_mbid = recording.mbid[:5] if recording.mbid else "[[ ]]" - print("%-60s %-50s %5s" % (rec_name[:59], artist[:49], rec_mbid), end='') + + text = "%-60s %-50s %5s" % (rec_name[:59], artist[:49], rec_mbid) if recording.artist is not None: if recording.artist.mbids is not None: - print(" %-20s" % ",".join([ mbid[:5] for mbid in recording.artist.mbids ]), end='') + text += " %-20s" % ",".join([ mbid[:5] for mbid in recording.artist.mbids ]) if recording.artist.artist_credit_id is not None: - print(" %8d" % recording.artist.artist_credit_id, end='') + text += " %8d" % recording.artist.artist_credit_id if self.print_year and recording.year is not None: - print(" %d" % recording.year, end='') + text += " %d" % recording.year if self.print_ranking: - print(" %.3f" % recording.ranking, end='') + text += " %.3f" % recording.ranking if self.print_listen_count or listen_count: - print(" %4d" % recording.listenbrainz['listen_count'], end='') + text += " %4d" % recording.listenbrainz['listen_count'] if self.print_bpm or bpm: - print(" %3d" % recording.acousticbrainz['bpm'], end='') + text += " %3d" % recording.acousticbrainz['bpm'] if self.print_popularity or popularity: - print(" %.3f" % recording.musicbrainz['popularity'], end='') + text += " %.3f" % recording.musicbrainz['popularity'] if self.print_latest_listened_at: if recording.listenbrainz["latest_listened_at"] is None: - print(" never ", end="") + text += " never " else: now = datetime.datetime.now() td = now - recording.listenbrainz["latest_listened_at"] - print(" %3d days " % td.days, end="") + text += " %3d days " % td.days if self.print_moods or moods: # TODO: make this print more than agg, but given the current state of moods/coverage... - print(" mood agg %3d" % int(100 * recording.acousticbrainz['moods']["mood_aggressive"]), end='') + text = " mood agg %3d" % int(100 * recording.acousticbrainz['moods']["mood_aggressive"]) if self.print_genre or genre: - print(" %s" % ",".join(recording.musicbrainz.get("genres", [])), end='') - print(" %s" % ",".join(recording.musicbrainz.get("tags", [])), end='') + text = " %s" % ",".join(recording.musicbrainz.get("genres", [])) + text = " %s" % ",".join(recording.musicbrainz.get("tags", [])) - print() + logger.info(text) def print(self, entity): """ Print out a list(Recording) or list(Playlist). """ @@ -121,4 +125,3 @@ def print(self, entity): self._print_recording(rec) raise PipelineError("You must pass a Recording or list of Recordings or a Playlist to print.") - diff --git a/troi/tools/spotify_lookup.py b/troi/tools/spotify_lookup.py index 95dc235f..36d7dd38 100644 --- a/troi/tools/spotify_lookup.py +++ b/troi/tools/spotify_lookup.py @@ -1,3 +1,4 @@ +import logging from collections import defaultdict import requests @@ -5,6 +6,8 @@ from more_itertools import chunked from spotipy import SpotifyException +logger = logging.getLogger(__name__) + SPOTIFY_IDS_LOOKUP_URL = "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json" @@ -129,7 +132,7 @@ def submit_to_spotify(spotify, playlist, spotify_user_id: str, is_public: bool = if len(spotify_track_ids) == 0: return None, None - print("submit %d tracks" % len(spotify_track_ids)) + logger.info("submit %d tracks" % len(spotify_track_ids)) playlist_id, playlist_url = None, None if existing_url: @@ -140,7 +143,7 @@ def submit_to_spotify(spotify, playlist, spotify_user_id: str, is_public: bool = spotify.playlist_change_details(playlist_id=playlist_id, name=playlist.name, description=playlist.description) except SpotifyException as err: # one possibility is that the user has deleted the spotify from playlist, so try creating a new one - print("provided playlist url has been unfollowed/deleted by the user, creating a new one") + logger.info("provided playlist url has been unfollowed/deleted by the user, creating a new one") playlist_id, playlist_url = None, None if not playlist_id: diff --git a/troi/utils.py b/troi/utils.py index a7bba815..9e82baba 100755 --- a/troi/utils.py +++ b/troi/utils.py @@ -1,9 +1,11 @@ import importlib import inspect +import logging import os import traceback import sys +logger = logging.getLogger(__name__) def discover_patches(): @@ -43,7 +45,7 @@ def discover_patches_from_dir(module_path, patch_dir, add_dot=False): try: patch = importlib.import_module(module_path + path[:-3]) except ImportError as err: - print("Cannot import %s, skipping:" % (path), file=sys.stderr) + logger.info("Cannot import %s, skipping:" % (path), file=sys.stderr) traceback.print_exc() continue