Esempio n. 1
0
    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        try:
            _, _, url = auth_client.get_authorize_url()
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'communication with Discogs failed')

        beets.ui.print_(u"To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_(u"Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError(u'Discogs authorization failed')
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'Discogs token request failed')

        # Save the token for later use.
        self._log.debug(u'Discogs token {0}, secret {1}', token, secret)
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret
Esempio n. 2
0
    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'apikey': 'rAzVUQYRaoFjeBjyWuWZ',
            'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy',
            'tokenfile': 'discogs_token.json',
            'source_weight': 0.5,
        })

        c_key = self.config['apikey'].get(unicode)
        c_secret = self.config['apisecret'].get(unicode)

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret, token,
                                     secret)
Esempio n. 3
0
    def setup(self, session=None):
        """Create the `discogs_client` field. Authenticate if necessary.
        """
        c_key = self.config['apikey'].as_str()
        c_secret = self.config['apisecret'].as_str()

        # Try using a configured user token (bypassing OAuth login).
        user_token = self.config['user_token'].as_str()
        if user_token:
            # The rate limit for authenticated users goes up to 60
            # requests per minute.
            self.rate_limit_per_minute = 60
            self.discogs_client = Client(USER_AGENT, user_token=user_token)
            return

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret,
                                     token, secret)
Esempio n. 4
0
    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        try:
            _, _, url = auth_client.get_authorize_url()
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'communication with Discogs failed')

        beets.ui.print_(u"To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_(u"Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError(u'Discogs authorization failed')
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'Discogs token request failed')

        # Save the token for later use.
        self._log.debug(u'Discogs token {0}, secret {1}', token, secret)
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret
Esempio n. 5
0
 def __init__(self):
     super(DiscogsPlugin, self).__init__()
     self.config.add({
         'source_weight': 0.5,
     })
     self.discogs_client = Client('beets/%s +http://beets.radbox.org/' %
                                  beets.__version__)
Esempio n. 6
0
    def setup(self, session=None):
        """Create the `discogs_client` field. Authenticate if necessary.
        """
        c_key = self.config['apikey'].as_str()
        c_secret = self.config['apisecret'].as_str()

        # Try using a configured user token (bypassing OAuth login).
        user_token = self.config['user_token'].as_str()
        if user_token:
            # The rate limit for authenticated users goes up to 60
            # requests per minute.
            self.rate_limit_per_minute = 60
            self.discogs_client = Client(USER_AGENT, user_token=user_token)
            return

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret, token,
                                     secret)
Esempio n. 7
0
    def test_user_agent(self):
        """User-Agent should be properly set"""
        self.d.artist(1).name

        bad_client = Client('')

        self.assertRaises(ConfigurationError, lambda: bad_client.artist(1).name)

        try:
            bad_client.artist(1).name
        except ConfigurationError as e:
            self.assertTrue('User-Agent' in str(e))
Esempio n. 8
0
    def test_user_agent(self):
        """User-Agent should be properly set"""
        self.d.artist(1).name

        bad_client = Client('')

        self.assertRaises(ConfigurationError,
                          lambda: bad_client.artist(1).name)

        try:
            bad_client.artist(1).name
        except ConfigurationError as e:
            self.assertTrue('User-Agent' in str(e))
Esempio n. 9
0
 def __init__(self):
     super(DiscogsPlugin, self).__init__()
     self.config.add({
         'source_weight': 0.5,
     })
     self.discogs_client = Client('beets/%s +http://beets.radbox.org/' %
                                  beets.__version__)
Esempio n. 10
0
    def __init__(self):
        self.consumer_key = 'pDGCsGkOvnpeoypQDyMp'
        self.consumer_secret = 'MXrkZgMMofvlTBVMWwGIYIgdfjXWXhvM'

        if self.is_authenticated():
            from access_tokens import access_token, access_secret

            self.discogs = Client(USER_AGENT,
                                  consumer_key=self.consumer_key,
                                  consumer_secret=self.consumer_secret,
                                  token=access_token,
                                  secret=access_secret)
        # If user does not have access token and secret we need to use OAuth
        else:
            self.discogs = Client(USER_AGENT)
            self.get_request_token()
Esempio n. 11
0
    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'apikey': 'rAzVUQYRaoFjeBjyWuWZ',
            'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy',
            'tokenfile': 'discogs_token.json',
            'source_weight': 0.5,
        })

        c_key = self.config['apikey'].get(unicode)
        c_secret = self.config['apisecret'].get(unicode)

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret,
                                     token, secret)
Esempio n. 12
0
    def setUp(self):

        # Filesystem client
        self.d = Client('test_client/0.1 +http://example.org')
        self.d._base_url = ''
        self.d._fetcher = LoggingDelegator(
            FilesystemFetcher(
                os.path.dirname(os.path.abspath(__file__)) + '/res'))
        self.d._verbose = True

        # Memory client
        responses = {
            '/artists/1': (b'{"id": 1, "name": "Badger"}', 200),
            '/500': (b'{"message": "mushroom"}', 500),
            '/204': (b'', 204),
        }
        self.m = Client('ua')
        self.m._base_url = ''
        self.m._fetcher = LoggingDelegator(MemoryFetcher(responses))
Esempio n. 13
0
    def setup(self, session=None):
        """Create the `discogs_client` field. Authenticate if necessary.
        """
        c_key = self.config['apikey'].as_str()
        c_secret = self.config['apisecret'].as_str()

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret, token,
                                     secret)
Esempio n. 14
0
def file_client():
    client = Client('test_client/0.1 +http://example.org')
    client._base_url = ''
    client._fetcher = LoggingDelegator(
        FilesystemFetcher(os.path.dirname(os.path.abspath(__file__)) + '/res'))
    client._verbose = True
    client.got = got
    client.posted = posted
    return client
Esempio n. 15
0
    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        _, _, url = auth_client.get_authorize_url()
        beets.ui.print_("To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_("Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError('Discogs authorization failed')

        # Save the token for later use.
        log.debug('Discogs token {0}, secret {1}'.format(token, secret))
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret
Esempio n. 16
0
    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        _, _, url = auth_client.get_authorize_url()
        beets.ui.print_("To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_("Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError('Discogs authorization failed')

        # Save the token for later use.
        log.debug('Discogs token {0}, secret {1}'.format(token, secret))
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret
Esempio n. 17
0
def memory_client():
    responses = {
        '/artists/1': (b'{"id": 1, "name": "Badger"}', 200),
        '/500': (b'{"message": "mushroom"}', 500),
        '/204': (b'', 204),
    }
    client = Client('ua')
    client._base_url = ''
    client._fetcher = LoggingDelegator(MemoryFetcher(responses))
    client.got = got
    client.posted = posted
    return client
Esempio n. 18
0
def test_user_agent(file_client):
    """User-Agent should be properly set"""
    file_client.artist(1).name

    bad_client = Client('')

    with pytest.raises(ConfigurationError):
        bad_client.artist(1).name

    try:
        bad_client.artist(1).name
    except ConfigurationError as e:
        assert 'User-Agent' in str(e)
Esempio n. 19
0
    def setup(self):
        """Create the `discogs_client` field. Authenticate if necessary.
        """
        c_key = self.config['apikey'].get(unicode)
        c_secret = self.config['apisecret'].get(unicode)

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret,
                                     token, secret)
Esempio n. 20
0
class DiscogsPlugin(BeetsPlugin):

    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'apikey': 'rAzVUQYRaoFjeBjyWuWZ',
            'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy',
            'tokenfile': 'discogs_token.json',
            'source_weight': 0.5,
        })
        self.config['apikey'].redact = True
        self.config['apisecret'].redact = True
        self.discogs_client = None
        self.register_listener('import_begin', self.setup)

    def setup(self, session=None):
        """Create the `discogs_client` field. Authenticate if necessary.
        """
        c_key = self.config['apikey'].as_str()
        c_secret = self.config['apisecret'].as_str()

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret,
                                     token, secret)

    def reset_auth(self):
        """Delete token file & redo the auth steps.
        """
        os.remove(self._tokenfile())
        self.setup()

    def _tokenfile(self):
        """Get the path to the JSON file for storing the OAuth token.
        """
        return self.config['tokenfile'].get(confit.Filename(in_app_dir=True))

    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        try:
            _, _, url = auth_client.get_authorize_url()
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'communication with Discogs failed')

        beets.ui.print_(u"To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_(u"Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError(u'Discogs authorization failed')
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'Discogs token request failed')

        # Save the token for later use.
        self._log.debug(u'Discogs token {0}, secret {1}', token, secret)
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret

    def album_distance(self, items, album_info, mapping):
        """Returns the album distance.
        """
        dist = Distance()
        if album_info.data_source == 'Discogs':
            dist.add('source', self.config['source_weight'].as_number())
        return dist

    def candidates(self, items, artist, album, va_likely):
        """Returns a list of AlbumInfo objects for discogs search results
        matching an album and artist (if not various).
        """
        if not self.discogs_client:
            return

        if va_likely:
            query = album
        else:
            query = '%s %s' % (artist, album)
        try:
            return self.get_albums(query)
        except DiscogsAPIError as e:
            self._log.debug(u'API Error: {0} (query: {1})', e, query)
            if e.status_code == 401:
                self.reset_auth()
                return self.candidates(items, artist, album, va_likely)
            else:
                return []
        except CONNECTION_ERRORS:
            self._log.debug(u'Connection error in album search', exc_info=True)
            return []

    def album_for_id(self, album_id):
        """Fetches an album by its Discogs ID and returns an AlbumInfo object
        or None if the album is not found.
        """
        if not self.discogs_client:
            return

        self._log.debug(u'Searching for release {0}', album_id)
        # Discogs-IDs are simple integers. We only look for those at the end
        # of an input string as to avoid confusion with other metadata plugins.
        # An optional bracket can follow the integer, as this is how discogs
        # displays the release ID on its webpage.
        match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
                          album_id)
        if not match:
            return None
        result = Release(self.discogs_client, {'id': int(match.group(2))})
        # Try to obtain title to verify that we indeed have a valid Release
        try:
            getattr(result, 'title')
        except DiscogsAPIError as e:
            if e.status_code != 404:
                self._log.debug(u'API Error: {0} (query: {1})', e, result._uri)
                if e.status_code == 401:
                    self.reset_auth()
                    return self.album_for_id(album_id)
            return None
        except CONNECTION_ERRORS:
            self._log.debug(u'Connection error in album lookup', exc_info=True)
            return None
        return self.get_album_info(result)

    def get_albums(self, query):
        """Returns a list of AlbumInfo objects for a discogs search query.
        """
        # Strip non-word characters from query. Things like "!" and "-" can
        # cause a query to return no results, even if they match the artist or
        # album title. Use `re.UNICODE` flag to avoid stripping non-english
        # word characters.
        # FIXME: Encode as ASCII to work around a bug:
        # https://github.com/beetbox/beets/issues/1051
        # When the library is fixed, we should encode as UTF-8.
        query = re.sub(r'(?u)\W+', ' ', query).encode('ascii', "replace")
        # Strip medium information from query, Things like "CD1" and "disk 1"
        # can also negate an otherwise positive result.
        query = re.sub(br'(?i)\b(CD|disc)\s*\d+', b'', query)
        try:
            releases = self.discogs_client.search(query,
                                                  type='release').page(1)
        except CONNECTION_ERRORS:
            self._log.debug(u"Communication error while searching for {0!r}",
                            query, exc_info=True)
            return []
        return [self.get_album_info(release) for release in releases[:5]]

    def get_album_info(self, result):
        """Returns an AlbumInfo object for a discogs Release object.
        """
        artist, artist_id = self.get_artist([a.data for a in result.artists])
        album = re.sub(r' +', ' ', result.title)
        album_id = result.data['id']
        # Use `.data` to access the tracklist directly instead of the
        # convenient `.tracklist` property, which will strip out useful artist
        # information and leave us with skeleton `Artist` objects that will
        # each make an API call just to get the same data back.
        tracks = self.get_tracks(result.data['tracklist'])
        albumtype = ', '.join(
            result.data['formats'][0].get('descriptions', [])) or None
        va = result.data['artists'][0]['name'].lower() == 'various'
        if va:
            artist = config['va_name'].as_str()
        year = result.data['year']
        label = result.data['labels'][0]['name']
        mediums = len(set(t.medium for t in tracks))
        catalogno = result.data['labels'][0]['catno']
        if catalogno == 'none':
            catalogno = None
        country = result.data.get('country')
        media = result.data['formats'][0]['name']
        # Explicitly set the `media` for the tracks, since it is expected by
        # `autotag.apply_metadata`, and set `medium_total`.
        for track in tracks:
            track.media = media
            track.medium_total = mediums
        data_url = result.data['uri']
        return AlbumInfo(album, album_id, artist, artist_id, tracks, asin=None,
                         albumtype=albumtype, va=va, year=year, month=None,
                         day=None, label=label, mediums=mediums,
                         artist_sort=None, releasegroup_id=None,
                         catalognum=catalogno, script=None, language=None,
                         country=country, albumstatus=None, media=media,
                         albumdisambig=None, artist_credit=None,
                         original_year=None, original_month=None,
                         original_day=None, data_source='Discogs',
                         data_url=data_url)

    def get_artist(self, artists):
        """Returns an artist string (all artists) and an artist_id (the main
        artist) for a list of discogs album or track artists.
        """
        artist_id = None
        bits = []
        for i, artist in enumerate(artists):
            if not artist_id:
                artist_id = artist['id']
            name = artist['name']
            # Strip disambiguation number.
            name = re.sub(r' \(\d+\)$', '', name)
            # Move articles to the front.
            name = re.sub(r'(?i)^(.*?), (a|an|the)$', r'\2 \1', name)
            bits.append(name)
            if artist['join'] and i < len(artists) - 1:
                bits.append(artist['join'])
        artist = ' '.join(bits).replace(' ,', ',') or None
        return artist, artist_id

    def get_tracks(self, tracklist):
        """Returns a list of TrackInfo objects for a discogs tracklist.
        """
        try:
            clean_tracklist = self.coalesce_tracks(tracklist)
        except Exception as exc:
            # FIXME: this is an extra precaution for making sure there are no
            # side effects after #2222. It should be removed after further
            # testing.
            self._log.debug(u'{}', traceback.format_exc())
            self._log.error(u'uncaught exception in coalesce_tracks: {}', exc)
            clean_tracklist = tracklist
        tracks = []
        index_tracks = {}
        index = 0
        for track in clean_tracklist:
            # Only real tracks have `position`. Otherwise, it's an index track.
            if track['position']:
                index += 1
                tracks.append(self.get_track_info(track, index))
            else:
                index_tracks[index + 1] = track['title']

        # Fix up medium and medium_index for each track. Discogs position is
        # unreliable, but tracks are in order.
        medium = None
        medium_count, index_count, side_count = 0, 0, 0
        sides_per_medium = 1

        # If a medium has two sides (ie. vinyl or cassette), each pair of
        # consecutive sides should belong to the same medium.
        if all([track.medium is not None for track in tracks]):
            m = sorted(set([track.medium.lower() for track in tracks]))
            # If all track.medium are single consecutive letters, assume it is
            # a 2-sided medium.
            if ''.join(m) in ascii_lowercase:
                sides_per_medium = 2
                side_count = 1  # Force for first item, where medium == None

        for track in tracks:
            # Handle special case where a different medium does not indicate a
            # new disc, when there is no medium_index and the ordinal of medium
            # is not sequential. For example, I, II, III, IV, V. Assume these
            # are the track index, not the medium.
            medium_is_index = track.medium and not track.medium_index and (
                len(track.medium) != 1 or
                ord(track.medium) - 64 != medium_count + 1
            )

            if not medium_is_index and medium != track.medium:
                if side_count < (sides_per_medium - 1):
                    # Increment side count: side changed, but not medium.
                    side_count += 1
                    medium = track.medium
                else:
                    # Increment medium_count and reset index_count and side
                    # count when medium changes.
                    medium = track.medium
                    medium_count += 1
                    index_count = 0
                    side_count = 0
            index_count += 1
            medium_count = 1 if medium_count == 0 else medium_count
            track.medium, track.medium_index = medium_count, index_count

        # Get `disctitle` from Discogs index tracks. Assume that an index track
        # before the first track of each medium is a disc title.
        for track in tracks:
            if track.medium_index == 1:
                if track.index in index_tracks:
                    disctitle = index_tracks[track.index]
                else:
                    disctitle = None
            track.disctitle = disctitle

        return tracks

    def coalesce_tracks(self, raw_tracklist):
        """Pre-process a tracklist, merging subtracks into a single track. The
        title for the merged track is the one from the previous index track,
        if present; otherwise it is a combination of the subtracks titles.
        """
        def add_merged_subtracks(tracklist, subtracks):
            """Modify `tracklist` in place, merging a list of `subtracks` into
            a single track into `tracklist`."""
            # Calculate position based on first subtrack, without subindex.
            idx, medium_idx, _ = self.get_track_index(subtracks[0]['position'])
            position = '%s%s' % (idx or '', medium_idx or '')

            if len(tracklist) > 1 and not tracklist[-1]['position']:
                # Assume the previous index track contains the track title, and
                # "convert" it to a real track. The only exception is if the
                # index track is the only one on the tracklist, as it probably
                # is a medium title.
                tracklist[-1]['position'] = position
            else:
                # Merge the subtracks, pick a title, and append the new track.
                track = subtracks[0].copy()
                track['title'] = ' / '.join([t['title'] for t in subtracks])
                tracklist.append(track)

        # Pre-process the tracklist, trying to identify subtracks.
        subtracks = []
        tracklist = []
        prev_subindex = ''
        for track in raw_tracklist:
            # Regular subtrack (track with subindex).
            if track['position']:
                _, _, subindex = self.get_track_index(track['position'])
                if subindex:
                    if subindex.rjust(len(raw_tracklist)) > prev_subindex:
                        # Subtrack still part of the current main track.
                        subtracks.append(track)
                    else:
                        # Subtrack part of a new group (..., 1.3, *2.1*, ...).
                        add_merged_subtracks(tracklist, subtracks)
                        subtracks = [track]
                    prev_subindex = subindex.rjust(len(raw_tracklist))
                    continue

            # Index track with nested sub_tracks.
            if not track['position'] and 'sub_tracks' in track:
                # Append the index track, assuming it contains the track title.
                tracklist.append(track)
                add_merged_subtracks(tracklist, track['sub_tracks'])
                continue

            # Regular track or index track without nested sub_tracks.
            if subtracks:
                add_merged_subtracks(tracklist, subtracks)
                subtracks = []
                prev_subindex = ''
            tracklist.append(track)

        # Merge and add the remaining subtracks, if any.
        if subtracks:
            add_merged_subtracks(tracklist, subtracks)

        return tracklist

    def get_track_info(self, track, index):
        """Returns a TrackInfo object for a discogs track.
        """
        title = track['title']
        track_id = None
        medium, medium_index, _ = self.get_track_index(track['position'])
        artist, artist_id = self.get_artist(track.get('artists', []))
        length = self.get_track_length(track['duration'])
        return TrackInfo(title, track_id, artist, artist_id, length, index,
                         medium, medium_index, artist_sort=None,
                         disctitle=None, artist_credit=None)

    def get_track_index(self, position):
        """Returns the medium, medium index and subtrack index for a discogs
        track position."""
        # Match the standard Discogs positions (12.2.9), which can have several
        # forms (1, 1-1, A1, A1.1, A1a, ...).
        match = re.match(
            r'^(.*?)'           # medium: everything before medium_index.
            r'(\d*?)'           # medium_index: a number at the end of
                                # `position`, except if followed by a subtrack
                                # index.
                                # subtrack_index: can only be matched if medium
                                # or medium_index have been matched, and can be
            r'((?<=\w)\.[\w]+'  # - a dot followed by a string (A.1, 2.A)
            r'|(?<=\d)[A-Z]+'   # - a string that follows a number (1A, B2a)
            r')?'
            r'$',
            position.upper()
        )

        if match:
            medium, index, subindex = match.groups()

            if subindex and subindex.startswith('.'):
                subindex = subindex[1:]
        else:
            self._log.debug(u'Invalid position: {0}', position)
            medium = index = subindex = None
        return medium or None, index or None, subindex or None

    def get_track_length(self, duration):
        """Returns the track length in seconds for a discogs duration.
        """
        try:
            length = time.strptime(duration, '%M:%S')
        except ValueError:
            return None
        return length.tm_min * 60 + length.tm_sec
Esempio n. 21
0
import os

import audio
import net
import utils

from dotenv import load_dotenv
from discogs_client import Client

load_dotenv()
DISCOGS_USER_TOKEN = os.getenv('DISCOGS_USER_TOKEN')
discogs_api = Client('Katalog', user_token=DISCOGS_USER_TOKEN)


def get_release(discogs_id):
    return discogs_api.release(discogs_id)


# release = get_release(34534)
# video_urls = [video.url for video in release.videos]
# styles = release.styles
# genres = release.genres

# audio.download_wav(video_urls[0])
# wav_filename = utils.video_url_to_wav_filename(video_urls[0])
# import IPython.display as play
# play.Audio(wav_filename)
# waveform, samplerate = net.load_tensor(wav_filename)
# print(40)
Esempio n. 22
0
class DiscogsPlugin(BeetsPlugin):

    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'apikey': 'rAzVUQYRaoFjeBjyWuWZ',
            'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy',
            'tokenfile': 'discogs_token.json',
            'source_weight': 0.5,
            'user_token': '',
        })
        self.config['apikey'].redact = True
        self.config['apisecret'].redact = True
        self.config['user_token'].redact = True
        self.discogs_client = None
        self.register_listener('import_begin', self.setup)
        self.rate_limit_per_minute = 25
        self.last_request_timestamp = 0

    def setup(self, session=None):
        """Create the `discogs_client` field. Authenticate if necessary.
        """
        c_key = self.config['apikey'].as_str()
        c_secret = self.config['apisecret'].as_str()

        # Try using a configured user token (bypassing OAuth login).
        user_token = self.config['user_token'].as_str()
        if user_token:
            # The rate limit for authenticated users goes up to 60
            # requests per minute.
            self.rate_limit_per_minute = 60
            self.discogs_client = Client(USER_AGENT, user_token=user_token)
            return

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret,
                                     token, secret)

    def _time_to_next_request(self):
        seconds_between_requests = 60 / self.rate_limit_per_minute
        seconds_since_last_request = time.time() - self.last_request_timestamp
        seconds_to_wait = seconds_between_requests - seconds_since_last_request
        return seconds_to_wait

    def request_start(self):
        """wait for rate limit if needed
        """
        time_to_next_request = self._time_to_next_request()
        if time_to_next_request > 0:
            self._log.debug('hit rate limit, waiting for {0} seconds',
                            time_to_next_request)
            time.sleep(time_to_next_request)

    def request_finished(self):
        """update timestamp for rate limiting
        """
        self.last_request_timestamp = time.time()

    def reset_auth(self):
        """Delete token file & redo the auth steps.
        """
        os.remove(self._tokenfile())
        self.setup()

    def _tokenfile(self):
        """Get the path to the JSON file for storing the OAuth token.
        """
        return self.config['tokenfile'].get(confit.Filename(in_app_dir=True))

    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        try:
            _, _, url = auth_client.get_authorize_url()
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'communication with Discogs failed')

        beets.ui.print_(u"To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_(u"Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError(u'Discogs authorization failed')
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'Discogs token request failed')

        # Save the token for later use.
        self._log.debug(u'Discogs token {0}, secret {1}', token, secret)
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret

    def album_distance(self, items, album_info, mapping):
        """Returns the album distance.
        """
        dist = Distance()
        if album_info.data_source == 'Discogs':
            dist.add('source', self.config['source_weight'].as_number())
        return dist

    def candidates(self, items, artist, album, va_likely):
        """Returns a list of AlbumInfo objects for discogs search results
        matching an album and artist (if not various).
        """
        if not self.discogs_client:
            return

        if va_likely:
            query = album
        else:
            query = '%s %s' % (artist, album)
        try:
            return self.get_albums(query)
        except DiscogsAPIError as e:
            self._log.debug(u'API Error: {0} (query: {1})', e, query)
            if e.status_code == 401:
                self.reset_auth()
                return self.candidates(items, artist, album, va_likely)
            else:
                return []
        except CONNECTION_ERRORS:
            self._log.debug(u'Connection error in album search', exc_info=True)
            return []

    def album_for_id(self, album_id):
        """Fetches an album by its Discogs ID and returns an AlbumInfo object
        or None if the album is not found.
        """
        if not self.discogs_client:
            return

        self._log.debug(u'Searching for release {0}', album_id)
        # Discogs-IDs are simple integers. We only look for those at the end
        # of an input string as to avoid confusion with other metadata plugins.
        # An optional bracket can follow the integer, as this is how discogs
        # displays the release ID on its webpage.
        match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
                          album_id)
        if not match:
            return None
        result = Release(self.discogs_client, {'id': int(match.group(2))})
        # Try to obtain title to verify that we indeed have a valid Release
        try:
            getattr(result, 'title')
        except DiscogsAPIError as e:
            if e.status_code != 404:
                self._log.debug(u'API Error: {0} (query: {1})', e, result._uri)
                if e.status_code == 401:
                    self.reset_auth()
                    return self.album_for_id(album_id)
            return None
        except CONNECTION_ERRORS:
            self._log.debug(u'Connection error in album lookup', exc_info=True)
            return None
        return self.get_album_info(result)

    def get_albums(self, query):
        """Returns a list of AlbumInfo objects for a discogs search query.
        """
        # Strip non-word characters from query. Things like "!" and "-" can
        # cause a query to return no results, even if they match the artist or
        # album title. Use `re.UNICODE` flag to avoid stripping non-english
        # word characters.
        # FIXME: Encode as ASCII to work around a bug:
        # https://github.com/beetbox/beets/issues/1051
        # When the library is fixed, we should encode as UTF-8.
        query = re.sub(r'(?u)\W+', ' ', query).encode('ascii', "replace")
        # Strip medium information from query, Things like "CD1" and "disk 1"
        # can also negate an otherwise positive result.
        query = re.sub(br'(?i)\b(CD|disc)\s*\d+', b'', query)

        self.request_start()
        try:
            releases = self.discogs_client.search(query,
                                                  type='release').page(1)
            self.request_finished()

        except CONNECTION_ERRORS:
            self._log.debug(u"Communication error while searching for {0!r}",
                            query, exc_info=True)
            return []
        return [album for album in map(self.get_album_info, releases[:5])
                if album]

    def get_master_year(self, master_id):
        """Fetches a master release given its Discogs ID and returns its year
        or None if the master release is not found.
        """
        self._log.debug(u'Searching for master release {0}', master_id)
        result = Master(self.discogs_client, {'id': master_id})

        self.request_start()
        try:
            year = result.fetch('year')
            self.request_finished()
            return year
        except DiscogsAPIError as e:
            if e.status_code != 404:
                self._log.debug(u'API Error: {0} (query: {1})', e, result._uri)
                if e.status_code == 401:
                    self.reset_auth()
                    return self.get_master_year(master_id)
            return None
        except CONNECTION_ERRORS:
            self._log.debug(u'Connection error in master release lookup',
                            exc_info=True)
            return None

    def get_album_info(self, result):
        """Returns an AlbumInfo object for a discogs Release object.
        """
        # Explicitly reload the `Release` fields, as they might not be yet
        # present if the result is from a `discogs_client.search()`.
        if not result.data.get('artists'):
            result.refresh()

        # Sanity check for required fields. The list of required fields is
        # defined at Guideline 1.3.1.a, but in practice some releases might be
        # lacking some of these fields. This function expects at least:
        # `artists` (>0), `title`, `id`, `tracklist` (>0)
        # https://www.discogs.com/help/doc/submission-guidelines-general-rules
        if not all([result.data.get(k) for k in ['artists', 'title', 'id',
                                                 'tracklist']]):
            self._log.warning(u"Release does not contain the required fields")
            return None

        artist, artist_id = self.get_artist([a.data for a in result.artists])
        album = re.sub(r' +', ' ', result.title)
        album_id = result.data['id']
        # Use `.data` to access the tracklist directly instead of the
        # convenient `.tracklist` property, which will strip out useful artist
        # information and leave us with skeleton `Artist` objects that will
        # each make an API call just to get the same data back.
        tracks = self.get_tracks(result.data['tracklist'])

        # Extract information for the optional AlbumInfo fields, if possible.
        va = result.data['artists'][0].get('name', '').lower() == 'various'
        year = result.data.get('year')
        mediums = [t.medium for t in tracks]
        country = result.data.get('country')
        data_url = result.data.get('uri')

        # Extract information for the optional AlbumInfo fields that are
        # contained on nested discogs fields.
        albumtype = media = label = catalogno = None
        if result.data.get('formats'):
            albumtype = ', '.join(
                result.data['formats'][0].get('descriptions', [])) or None
            media = result.data['formats'][0]['name']
        if result.data.get('labels'):
            label = result.data['labels'][0].get('name')
            catalogno = result.data['labels'][0].get('catno')

        # Additional cleanups (various artists name, catalog number, media).
        if va:
            artist = config['va_name'].as_str()
        if catalogno == 'none':
            catalogno = None
        # Explicitly set the `media` for the tracks, since it is expected by
        # `autotag.apply_metadata`, and set `medium_total`.
        for track in tracks:
            track.media = media
            track.medium_total = mediums.count(track.medium)
            # Discogs does not have track IDs. Invent our own IDs as proposed
            # in #2336.
            track.track_id = str(album_id) + "-" + track.track_alt

        # Retrieve master release id (returns None if there isn't one).
        master_id = result.data.get('master_id')
        # Assume `original_year` is equal to `year` for releases without
        # a master release, otherwise fetch the master release.
        original_year = self.get_master_year(master_id) if master_id else year

        return AlbumInfo(album, album_id, artist, artist_id, tracks, asin=None,
                         albumtype=albumtype, va=va, year=year, month=None,
                         day=None, label=label, mediums=len(set(mediums)),
                         artist_sort=None, releasegroup_id=master_id,
                         catalognum=catalogno, script=None, language=None,
                         country=country, albumstatus=None, media=media,
                         albumdisambig=None, artist_credit=None,
                         original_year=original_year, original_month=None,
                         original_day=None, data_source='Discogs',
                         data_url=data_url)

    def get_artist(self, artists):
        """Returns an artist string (all artists) and an artist_id (the main
        artist) for a list of discogs album or track artists.
        """
        artist_id = None
        bits = []
        for i, artist in enumerate(artists):
            if not artist_id:
                artist_id = artist['id']
            name = artist['name']
            # Strip disambiguation number.
            name = re.sub(r' \(\d+\)$', '', name)
            # Move articles to the front.
            name = re.sub(r'(?i)^(.*?), (a|an|the)$', r'\2 \1', name)
            bits.append(name)
            if artist['join'] and i < len(artists) - 1:
                bits.append(artist['join'])
        artist = ' '.join(bits).replace(' ,', ',') or None
        return artist, artist_id

    def get_tracks(self, tracklist):
        """Returns a list of TrackInfo objects for a discogs tracklist.
        """
        try:
            clean_tracklist = self.coalesce_tracks(tracklist)
        except Exception as exc:
            # FIXME: this is an extra precaution for making sure there are no
            # side effects after #2222. It should be removed after further
            # testing.
            self._log.debug(u'{}', traceback.format_exc())
            self._log.error(u'uncaught exception in coalesce_tracks: {}', exc)
            clean_tracklist = tracklist
        tracks = []
        index_tracks = {}
        index = 0
        for track in clean_tracklist:
            # Only real tracks have `position`. Otherwise, it's an index track.
            if track['position']:
                index += 1
                track_info = self.get_track_info(track, index)
                track_info.track_alt = track['position']
                tracks.append(track_info)
            else:
                index_tracks[index + 1] = track['title']

        # Fix up medium and medium_index for each track. Discogs position is
        # unreliable, but tracks are in order.
        medium = None
        medium_count, index_count, side_count = 0, 0, 0
        sides_per_medium = 1

        # If a medium has two sides (ie. vinyl or cassette), each pair of
        # consecutive sides should belong to the same medium.
        if all([track.medium is not None for track in tracks]):
            m = sorted(set([track.medium.lower() for track in tracks]))
            # If all track.medium are single consecutive letters, assume it is
            # a 2-sided medium.
            if ''.join(m) in ascii_lowercase:
                sides_per_medium = 2

        for track in tracks:
            # Handle special case where a different medium does not indicate a
            # new disc, when there is no medium_index and the ordinal of medium
            # is not sequential. For example, I, II, III, IV, V. Assume these
            # are the track index, not the medium.
            # side_count is the number of mediums or medium sides (in the case
            # of two-sided mediums) that were seen before.
            medium_is_index = track.medium and not track.medium_index and (
                len(track.medium) != 1 or
                # Not within standard incremental medium values (A, B, C, ...).
                ord(track.medium) - 64 != side_count + 1
            )

            if not medium_is_index and medium != track.medium:
                side_count += 1
                if sides_per_medium == 2:
                    if side_count % sides_per_medium:
                        # Two-sided medium changed. Reset index_count.
                        index_count = 0
                        medium_count += 1
                else:
                    # Medium changed. Reset index_count.
                    medium_count += 1
                    index_count = 0
                medium = track.medium

            index_count += 1
            medium_count = 1 if medium_count == 0 else medium_count
            track.medium, track.medium_index = medium_count, index_count

        # Get `disctitle` from Discogs index tracks. Assume that an index track
        # before the first track of each medium is a disc title.
        for track in tracks:
            if track.medium_index == 1:
                if track.index in index_tracks:
                    disctitle = index_tracks[track.index]
                else:
                    disctitle = None
            track.disctitle = disctitle

        return tracks

    def coalesce_tracks(self, raw_tracklist):
        """Pre-process a tracklist, merging subtracks into a single track. The
        title for the merged track is the one from the previous index track,
        if present; otherwise it is a combination of the subtracks titles.
        """
        def add_merged_subtracks(tracklist, subtracks):
            """Modify `tracklist` in place, merging a list of `subtracks` into
            a single track into `tracklist`."""
            # Calculate position based on first subtrack, without subindex.
            idx, medium_idx, sub_idx = \
                self.get_track_index(subtracks[0]['position'])
            position = '%s%s' % (idx or '', medium_idx or '')

            if tracklist and not tracklist[-1]['position']:
                # Assume the previous index track contains the track title.
                if sub_idx:
                    # "Convert" the track title to a real track, discarding the
                    # subtracks assuming they are logical divisions of a
                    # physical track (12.2.9 Subtracks).
                    tracklist[-1]['position'] = position
                else:
                    # Promote the subtracks to real tracks, discarding the
                    # index track, assuming the subtracks are physical tracks.
                    index_track = tracklist.pop()
                    # Fix artists when they are specified on the index track.
                    if index_track.get('artists'):
                        for subtrack in subtracks:
                            if not subtrack.get('artists'):
                                subtrack['artists'] = index_track['artists']
                    tracklist.extend(subtracks)
            else:
                # Merge the subtracks, pick a title, and append the new track.
                track = subtracks[0].copy()
                track['title'] = ' / '.join([t['title'] for t in subtracks])
                tracklist.append(track)

        # Pre-process the tracklist, trying to identify subtracks.
        subtracks = []
        tracklist = []
        prev_subindex = ''
        for track in raw_tracklist:
            # Regular subtrack (track with subindex).
            if track['position']:
                _, _, subindex = self.get_track_index(track['position'])
                if subindex:
                    if subindex.rjust(len(raw_tracklist)) > prev_subindex:
                        # Subtrack still part of the current main track.
                        subtracks.append(track)
                    else:
                        # Subtrack part of a new group (..., 1.3, *2.1*, ...).
                        add_merged_subtracks(tracklist, subtracks)
                        subtracks = [track]
                    prev_subindex = subindex.rjust(len(raw_tracklist))
                    continue

            # Index track with nested sub_tracks.
            if not track['position'] and 'sub_tracks' in track:
                # Append the index track, assuming it contains the track title.
                tracklist.append(track)
                add_merged_subtracks(tracklist, track['sub_tracks'])
                continue

            # Regular track or index track without nested sub_tracks.
            if subtracks:
                add_merged_subtracks(tracklist, subtracks)
                subtracks = []
                prev_subindex = ''
            tracklist.append(track)

        # Merge and add the remaining subtracks, if any.
        if subtracks:
            add_merged_subtracks(tracklist, subtracks)

        return tracklist

    def get_track_info(self, track, index):
        """Returns a TrackInfo object for a discogs track.
        """
        title = track['title']
        track_id = None
        medium, medium_index, _ = self.get_track_index(track['position'])
        artist, artist_id = self.get_artist(track.get('artists', []))
        length = self.get_track_length(track['duration'])
        return TrackInfo(title, track_id, artist=artist, artist_id=artist_id,
                         length=length, index=index,
                         medium=medium, medium_index=medium_index,
                         artist_sort=None, disctitle=None, artist_credit=None)

    def get_track_index(self, position):
        """Returns the medium, medium index and subtrack index for a discogs
        track position."""
        # Match the standard Discogs positions (12.2.9), which can have several
        # forms (1, 1-1, A1, A1.1, A1a, ...).
        match = re.match(
            r'^(.*?)'           # medium: everything before medium_index.
            r'(\d*?)'           # medium_index: a number at the end of
                                # `position`, except if followed by a subtrack
                                # index.
                                # subtrack_index: can only be matched if medium
                                # or medium_index have been matched, and can be
            r'((?<=\w)\.[\w]+'  # - a dot followed by a string (A.1, 2.A)
            r'|(?<=\d)[A-Z]+'   # - a string that follows a number (1A, B2a)
            r')?'
            r'$',
            position.upper()
        )

        if match:
            medium, index, subindex = match.groups()

            if subindex and subindex.startswith('.'):
                subindex = subindex[1:]
        else:
            self._log.debug(u'Invalid position: {0}', position)
            medium = index = subindex = None
        return medium or None, index or None, subindex or None

    def get_track_length(self, duration):
        """Returns the track length in seconds for a discogs duration.
        """
        try:
            length = time.strptime(duration, '%M:%S')
        except ValueError:
            return None
        return length.tm_min * 60 + length.tm_sec
Esempio n. 23
0
class DiscogsPlugin(BeetsPlugin):
    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'source_weight': 0.5,
        })
        self.discogs_client = Client('beets/%s +http://beets.radbox.org/' %
                                     beets.__version__)

    def album_distance(self, items, album_info, mapping):
        """Returns the album distance.
        """
        dist = Distance()
        if album_info.data_source == 'Discogs':
            dist.add('source', self.config['source_weight'].as_number())
        return dist

    def candidates(self, items, artist, album, va_likely):
        """Returns a list of AlbumInfo objects for discogs search results
        matching an album and artist (if not various).
        """
        if va_likely:
            query = album
        else:
            query = '%s %s' % (artist, album)
        try:
            return self.get_albums(query)
        except DiscogsAPIError as e:
            log.debug('Discogs API Error: %s (query: %s' % (e, query))
            return []

    def album_for_id(self, album_id):
        """Fetches an album by its Discogs ID and returns an AlbumInfo object
        or None if the album is not found.
        """
        log.debug('Searching discogs for release %s' % str(album_id))
        # Discogs-IDs are simple integers. We only look for those at the end
        # of an input string as to avoid confusion with other metadata plugins.
        # An optional bracket can follow the integer, as this is how discogs
        # displays the release ID on its webpage.
        match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
                          album_id)
        if not match:
            return None
        result = Release(self.discogs_client, {'id': int(match.group(2))})
        # Try to obtain title to verify that we indeed have a valid Release
        try:
            getattr(result, 'title')
        except DiscogsAPIError as e:
            if e.message != '404 Not Found':
                log.debug('Discogs API Error: %s (query: %s)' %
                          (e, result._uri))
            return None
        return self.get_album_info(result)

    def get_albums(self, query):
        """Returns a list of AlbumInfo objects for a discogs search query.
        """
        # Strip non-word characters from query. Things like "!" and "-" can
        # cause a query to return no results, even if they match the artist or
        # album title. Use `re.UNICODE` flag to avoid stripping non-english
        # word characters.
        query = re.sub(r'(?u)\W+', ' ', query).encode('utf8')
        # Strip medium information from query, Things like "CD1" and "disk 1"
        # can also negate an otherwise positive result.
        query = re.sub(r'(?i)\b(CD|disc)\s*\d+', '', query)
        releases = self.discogs_client.search(query, type='release').page(1)
        return [self.get_album_info(release) for release in releases[:5]]

    def get_album_info(self, result):
        """Returns an AlbumInfo object for a discogs Release object.
        """
        artist, artist_id = self.get_artist([a.data for a in result.artists])
        album = re.sub(r' +', ' ', result.title)
        album_id = result.data['id']
        # Use `.data` to access the tracklist directly instead of the
        # convenient `.tracklist` property, which will strip out useful artist
        # information and leave us with skeleton `Artist` objects that will
        # each make an API call just to get the same data back.
        tracks = self.get_tracks(result.data['tracklist'])
        albumtype = ', '.join(result.data['formats'][0].get(
            'descriptions', [])) or None
        va = result.data['artists'][0]['name'].lower() == 'various'
        year = result.data['year']
        label = result.data['labels'][0]['name']
        mediums = len(set(t.medium for t in tracks))
        catalogno = result.data['labels'][0]['catno']
        if catalogno == 'none':
            catalogno = None
        country = result.data.get('country')
        media = result.data['formats'][0]['name']
        data_url = result.data['uri']
        return AlbumInfo(album,
                         album_id,
                         artist,
                         artist_id,
                         tracks,
                         asin=None,
                         albumtype=albumtype,
                         va=va,
                         year=year,
                         month=None,
                         day=None,
                         label=label,
                         mediums=mediums,
                         artist_sort=None,
                         releasegroup_id=None,
                         catalognum=catalogno,
                         script=None,
                         language=None,
                         country=country,
                         albumstatus=None,
                         media=media,
                         albumdisambig=None,
                         artist_credit=None,
                         original_year=None,
                         original_month=None,
                         original_day=None,
                         data_source='Discogs',
                         data_url=data_url)

    def get_artist(self, artists):
        """Returns an artist string (all artists) and an artist_id (the main
        artist) for a list of discogs album or track artists.
        """
        artist_id = None
        bits = []
        for artist in artists:
            if not artist_id:
                artist_id = artist['id']
            name = artist['name']
            # Strip disambiguation number.
            name = re.sub(r' \(\d+\)$', '', name)
            # Move articles to the front.
            name = re.sub(r'(?i)^(.*?), (a|an|the)$', r'\2 \1', name)
            bits.append(name)
            if artist['join']:
                bits.append(artist['join'])
        artist = ' '.join(bits).replace(' ,', ',') or None
        return artist, artist_id

    def get_tracks(self, tracklist):
        """Returns a list of TrackInfo objects for a discogs tracklist.
        """
        tracks = []
        index_tracks = {}
        index = 0
        for track in tracklist:
            # Only real tracks have `position`. Otherwise, it's an index track.
            if track['position']:
                index += 1
                tracks.append(self.get_track_info(track, index))
            else:
                index_tracks[index + 1] = track['title']

        # Fix up medium and medium_index for each track. Discogs position is
        # unreliable, but tracks are in order.
        medium = None
        medium_count, index_count = 0, 0
        for track in tracks:
            # Handle special case where a different medium does not indicate a
            # new disc, when there is no medium_index and the ordinal of medium
            # is not sequential. For example, I, II, III, IV, V. Assume these
            # are the track index, not the medium.
            medium_is_index = track.medium and not track.medium_index and (
                len(track.medium) != 1
                or ord(track.medium) - 64 != medium_count + 1)

            if not medium_is_index and medium != track.medium:
                # Increment medium_count and reset index_count when medium
                # changes.
                medium = track.medium
                medium_count += 1
                index_count = 0
            index_count += 1
            track.medium, track.medium_index = medium_count, index_count

        # Get `disctitle` from Discogs index tracks. Assume that an index track
        # before the first track of each medium is a disc title.
        for track in tracks:
            if track.medium_index == 1:
                if track.index in index_tracks:
                    disctitle = index_tracks[track.index]
                else:
                    disctitle = None
            track.disctitle = disctitle

        return tracks

    def get_track_info(self, track, index):
        """Returns a TrackInfo object for a discogs track.
        """
        title = track['title']
        track_id = None
        medium, medium_index = self.get_track_index(track['position'])
        artist, artist_id = self.get_artist(track.get('artists', []))
        length = self.get_track_length(track['duration'])
        return TrackInfo(title,
                         track_id,
                         artist,
                         artist_id,
                         length,
                         index,
                         medium,
                         medium_index,
                         artist_sort=None,
                         disctitle=None,
                         artist_credit=None)

    def get_track_index(self, position):
        """Returns the medium and medium index for a discogs track position.
        """
        # medium_index is a number at the end of position. medium is everything
        # else. E.g. (A)(1), (Side A, Track )(1), (A)(), ()(1), etc.
        match = re.match(r'^(.*?)(\d*)$', position.upper())
        if match:
            medium, index = match.groups()
        else:
            log.debug('Invalid discogs position: %s' % position)
            medium = index = None
        return medium or None, index or None

    def get_track_length(self, duration):
        """Returns the track length in seconds for a discogs duration.
        """
        try:
            length = time.strptime(duration, '%M:%S')
        except ValueError:
            return None
        return length.tm_min * 60 + length.tm_sec
Esempio n. 24
0
class Discogs:
    """
    OAuth authentication wrapper for discogs API with 'search' method to 
    search for cover images.
    """
    def __init__(self):
        self.consumer_key = 'pDGCsGkOvnpeoypQDyMp'
        self.consumer_secret = 'MXrkZgMMofvlTBVMWwGIYIgdfjXWXhvM'

        if self.is_authenticated():
            from access_tokens import access_token, access_secret

            self.discogs = Client(USER_AGENT,
                                  consumer_key=self.consumer_key,
                                  consumer_secret=self.consumer_secret,
                                  token=access_token,
                                  secret=access_secret)
        # If user does not have access token and secret we need to use OAuth
        else:
            self.discogs = Client(USER_AGENT)
            self.get_request_token()

    def get_request_token(self):
        self.discogs.set_consumer_key(self.consumer_key, self.consumer_secret)
        token, secret, url = self.discogs.get_authorize_url()

        auth = False
        while not auth:
            print('in order to access your access images from discogs you')
            print('need to verify your Discogs account using OAuth.')
            print(f'please visit {url} and accept the authentication request')
            oauth_code = input('Verification code: ')

            try:
                token, secret = self.discogs.get_access_token(oauth_code)
            except Exception as e:
                print(f'Unable to authenticate, please try again. error="{e}"')
                continue

            if token:
                auth = True
        with open('access_tokens.py', 'w') as f:
            f.write('access token: {token}')
            f.write('access secret: {secret}')

    def is_authenticated(self):
        """
        Returns True if token exists in local file
        """
        if os.path.isfile('access_tokens.py'):
            return True

    def search(self, params):
        """
        takes search params as param, col dict and returns image & image id
        """
        url = 'https://api.discogs.com/database/search?'
        for key, value in params.items():
            url += key + '=' + value + '&'

        url += 'key=' + self.consumer_key
        url += '&secret=' + self.consumer_secret

        response = requests.get(url).text
        data = json.loads(response)
        try:
            item = data['results'][0]
        except IndexError:
            print(f"Nothing found for {params['artist']}")
            return None

        time.sleep(1.1)
        cover_url = item['cover_image']
        cover_id = str(item['id'])
        response = requests.get(cover_url, stream=True)
        if response.status_code == 200:
            print(f"Downloaded image for {params['artist']}, id:{cover_id}")
            return response, cover_id
        else:
            print(f"Error getting {params['artist']}: {response.status_code}")
Esempio n. 25
0
 def __init__(self, user_token=None):
     self.client = Client(Discogs.APP_VERSION, user_token=user_token)
Esempio n. 26
0
import time
import os

from discogs_client import Client
from discogs_client.exceptions import HTTPError

user_token = os.environ.get('DISCOGS_TOKEN')

dgs_client = Client('EventbriteTest/1.2.1', user_token=user_token)


class DiscoGSError(RuntimeError):
    pass


def retryer(max_retries=10, timeout=5):
    def wraps(func):
        def inner(*args, **kwargs):
            for i in range(max_retries):
                try:
                    result = func(*args, **kwargs)
                except HTTPError:
                    time.sleep(timeout)
                    continue
                else:
                    return result
            else:
                raise DiscoGSError

        return inner
Esempio n. 27
0
    app = QtGui.QApplication(sys.argv)
    iconpath = resource_path(os.path.join(ICN_DIR, 'discopy.ico'))

    start = time()
    splashpath = resource_path(os.path.join(ICN_DIR, SPLSH_SCRN))
    splash = QtGui.QSplashScreen(QtGui.QPixmap(splashpath))
    splash.show()
    while time() - start < 2:
        sleep(0.001)
        app.processEvents()

    win = DiscoPy(
        Ui_MainWindow(),
        settingsHandler,
        Client('discopy/0.1',
        CONSUMER_KEY, CONSUMER_SECRET,
        TOKEN, SECRET),
        NameBuilder(),
        TagData,
        ImageHandler())

    win.setWindowIcon(QtGui.QIcon(iconpath))
    win.set_rename_dialog(RenameDialog(settingsHandler, win))
    splash.finish(win)

    # import ctypes
    # appid = 'discopy.0.1'
    # ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid)

    win.center()
    win.show()
Esempio n. 28
0
class DiscogsPlugin(BeetsPlugin):
    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'apikey': 'rAzVUQYRaoFjeBjyWuWZ',
            'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy',
            'tokenfile': 'discogs_token.json',
            'source_weight': 0.5,
        })

        c_key = self.config['apikey'].get(unicode)
        c_secret = self.config['apisecret'].get(unicode)

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret, token,
                                     secret)

    def _tokenfile(self):
        """Get the path to the JSON file for storing the OAuth token.
        """
        return self.config['tokenfile'].get(confit.Filename(in_app_dir=True))

    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        _, _, url = auth_client.get_authorize_url()
        beets.ui.print_("To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_("Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError('Discogs authorization failed')

        # Save the token for later use.
        log.debug('Discogs token {0}, secret {1}'.format(token, secret))
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret

    def album_distance(self, items, album_info, mapping):
        """Returns the album distance.
        """
        dist = Distance()
        if album_info.data_source == 'Discogs':
            dist.add('source', self.config['source_weight'].as_number())
        return dist

    def candidates(self, items, artist, album, va_likely):
        """Returns a list of AlbumInfo objects for discogs search results
        matching an album and artist (if not various).
        """
        if va_likely:
            query = album
        else:
            query = '%s %s' % (artist, album)
        try:
            return self.get_albums(query)
        except DiscogsAPIError as e:
            log.debug(u'Discogs API Error: {0} (query: {1})'.format(e, query))
            return []
        except ConnectionError as e:
            log.debug(u'HTTP Connection Error: {0}'.format(e))
            return []

    def album_for_id(self, album_id):
        """Fetches an album by its Discogs ID and returns an AlbumInfo object
        or None if the album is not found.
        """
        log.debug(u'Searching Discogs for release {0}'.format(str(album_id)))
        # Discogs-IDs are simple integers. We only look for those at the end
        # of an input string as to avoid confusion with other metadata plugins.
        # An optional bracket can follow the integer, as this is how discogs
        # displays the release ID on its webpage.
        match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
                          album_id)
        if not match:
            return None
        result = Release(self.discogs_client, {'id': int(match.group(2))})
        # Try to obtain title to verify that we indeed have a valid Release
        try:
            getattr(result, 'title')
        except DiscogsAPIError as e:
            if e.message != '404 Not Found':
                log.debug(u'Discogs API Error: {0} (query: {1})'.format(
                    e, result._uri))
            return None
        except ConnectionError as e:
            log.debug(u'HTTP Connection Error: {0}'.format(e))
            return None
        return self.get_album_info(result)

    def get_albums(self, query):
        """Returns a list of AlbumInfo objects for a discogs search query.
        """
        # Strip non-word characters from query. Things like "!" and "-" can
        # cause a query to return no results, even if they match the artist or
        # album title. Use `re.UNICODE` flag to avoid stripping non-english
        # word characters.
        # TEMPORARY: Encode as ASCII to work around a bug:
        # https://github.com/sampsyo/beets/issues/1051
        # When the library is fixed, we should encode as UTF-8.
        query = re.sub(r'(?u)\W+', ' ', query).encode('ascii', "replace")
        # Strip medium information from query, Things like "CD1" and "disk 1"
        # can also negate an otherwise positive result.
        query = re.sub(r'(?i)\b(CD|disc)\s*\d+', '', query)
        releases = self.discogs_client.search(query, type='release').page(1)
        return [self.get_album_info(release) for release in releases[:5]]

    def get_album_info(self, result):
        """Returns an AlbumInfo object for a discogs Release object.
        """
        artist, artist_id = self.get_artist([a.data for a in result.artists])
        album = re.sub(r' +', ' ', result.title)
        album_id = result.data['id']
        # Use `.data` to access the tracklist directly instead of the
        # convenient `.tracklist` property, which will strip out useful artist
        # information and leave us with skeleton `Artist` objects that will
        # each make an API call just to get the same data back.
        tracks = self.get_tracks(result.data['tracklist'])
        albumtype = ', '.join(result.data['formats'][0].get(
            'descriptions', [])) or None
        va = result.data['artists'][0]['name'].lower() == 'various'
        year = result.data['year']
        label = result.data['labels'][0]['name']
        mediums = len(set(t.medium for t in tracks))
        catalogno = result.data['labels'][0]['catno']
        if catalogno == 'none':
            catalogno = None
        country = result.data.get('country')
        media = result.data['formats'][0]['name']
        data_url = result.data['uri']
        return AlbumInfo(album,
                         album_id,
                         artist,
                         artist_id,
                         tracks,
                         asin=None,
                         albumtype=albumtype,
                         va=va,
                         year=year,
                         month=None,
                         day=None,
                         label=label,
                         mediums=mediums,
                         artist_sort=None,
                         releasegroup_id=None,
                         catalognum=catalogno,
                         script=None,
                         language=None,
                         country=country,
                         albumstatus=None,
                         media=media,
                         albumdisambig=None,
                         artist_credit=None,
                         original_year=None,
                         original_month=None,
                         original_day=None,
                         data_source='Discogs',
                         data_url=data_url)

    def get_artist(self, artists):
        """Returns an artist string (all artists) and an artist_id (the main
        artist) for a list of discogs album or track artists.
        """
        artist_id = None
        bits = []
        for i, artist in enumerate(artists):
            if not artist_id:
                artist_id = artist['id']
            name = artist['name']
            # Strip disambiguation number.
            name = re.sub(r' \(\d+\)$', '', name)
            # Move articles to the front.
            name = re.sub(r'(?i)^(.*?), (a|an|the)$', r'\2 \1', name)
            bits.append(name)
            if artist['join'] and i < len(artists) - 1:
                bits.append(artist['join'])
        artist = ' '.join(bits).replace(' ,', ',') or None
        return artist, artist_id

    def get_tracks(self, tracklist):
        """Returns a list of TrackInfo objects for a discogs tracklist.
        """
        tracks = []
        index_tracks = {}
        index = 0
        for track in tracklist:
            # Only real tracks have `position`. Otherwise, it's an index track.
            if track['position']:
                index += 1
                tracks.append(self.get_track_info(track, index))
            else:
                index_tracks[index + 1] = track['title']

        # Fix up medium and medium_index for each track. Discogs position is
        # unreliable, but tracks are in order.
        medium = None
        medium_count, index_count = 0, 0
        for track in tracks:
            # Handle special case where a different medium does not indicate a
            # new disc, when there is no medium_index and the ordinal of medium
            # is not sequential. For example, I, II, III, IV, V. Assume these
            # are the track index, not the medium.
            medium_is_index = track.medium and not track.medium_index and (
                len(track.medium) != 1
                or ord(track.medium) - 64 != medium_count + 1)

            if not medium_is_index and medium != track.medium:
                # Increment medium_count and reset index_count when medium
                # changes.
                medium = track.medium
                medium_count += 1
                index_count = 0
            index_count += 1
            track.medium, track.medium_index = medium_count, index_count

        # Get `disctitle` from Discogs index tracks. Assume that an index track
        # before the first track of each medium is a disc title.
        for track in tracks:
            if track.medium_index == 1:
                if track.index in index_tracks:
                    disctitle = index_tracks[track.index]
                else:
                    disctitle = None
            track.disctitle = disctitle

        return tracks

    def get_track_info(self, track, index):
        """Returns a TrackInfo object for a discogs track.
        """
        title = track['title']
        track_id = None
        medium, medium_index = self.get_track_index(track['position'])
        artist, artist_id = self.get_artist(track.get('artists', []))
        length = self.get_track_length(track['duration'])
        return TrackInfo(title,
                         track_id,
                         artist,
                         artist_id,
                         length,
                         index,
                         medium,
                         medium_index,
                         artist_sort=None,
                         disctitle=None,
                         artist_credit=None)

    def get_track_index(self, position):
        """Returns the medium and medium index for a discogs track position.
        """
        # medium_index is a number at the end of position. medium is everything
        # else. E.g. (A)(1), (Side A, Track )(1), (A)(), ()(1), etc.
        match = re.match(r'^(.*?)(\d*)$', position.upper())
        if match:
            medium, index = match.groups()
        else:
            log.debug(u'Invalid Discogs position: {0}'.format(position))
            medium = index = None
        return medium or None, index or None

    def get_track_length(self, duration):
        """Returns the track length in seconds for a discogs duration.
        """
        try:
            length = time.strptime(duration, '%M:%S')
        except ValueError:
            return None
        return length.tm_min * 60 + length.tm_sec
Esempio n. 29
0
class DiscogsPlugin(BeetsPlugin):
    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'apikey': API_KEY,
            'apisecret': API_SECRET,
            'tokenfile': 'discogs_token.json',
            'source_weight': 0.5,
            'user_token': '',
            'separator': u', ',
            'index_tracks': False,
        })
        self.config['apikey'].redact = True
        self.config['apisecret'].redact = True
        self.config['user_token'].redact = True
        self.discogs_client = None
        self.register_listener('import_begin', self.setup)
        self.rate_limit_per_minute = 25
        self.last_request_timestamp = 0

    def setup(self, session=None):
        """Create the `discogs_client` field. Authenticate if necessary.
        """
        c_key = self.config['apikey'].as_str()
        c_secret = self.config['apisecret'].as_str()

        # Try using a configured user token (bypassing OAuth login).
        user_token = self.config['user_token'].as_str()
        if user_token:
            # The rate limit for authenticated users goes up to 60
            # requests per minute.
            self.rate_limit_per_minute = 60
            self.discogs_client = Client(USER_AGENT, user_token=user_token)
            return

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret, token,
                                     secret)

    def _time_to_next_request(self):
        seconds_between_requests = 60 / self.rate_limit_per_minute
        seconds_since_last_request = time.time() - self.last_request_timestamp
        seconds_to_wait = seconds_between_requests - seconds_since_last_request
        return seconds_to_wait

    def request_start(self):
        """wait for rate limit if needed
        """
        time_to_next_request = self._time_to_next_request()
        if time_to_next_request > 0:
            self._log.debug('hit rate limit, waiting for {0} seconds',
                            time_to_next_request)
            time.sleep(time_to_next_request)

    def request_finished(self):
        """update timestamp for rate limiting
        """
        self.last_request_timestamp = time.time()

    def reset_auth(self):
        """Delete token file & redo the auth steps.
        """
        os.remove(self._tokenfile())
        self.setup()

    def _tokenfile(self):
        """Get the path to the JSON file for storing the OAuth token.
        """
        return self.config['tokenfile'].get(confuse.Filename(in_app_dir=True))

    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        try:
            _, _, url = auth_client.get_authorize_url()
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'communication with Discogs failed')

        beets.ui.print_(u"To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_(u"Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError(u'Discogs authorization failed')
        except CONNECTION_ERRORS as e:
            self._log.debug(u'connection error: {0}', e)
            raise beets.ui.UserError(u'Discogs token request failed')

        # Save the token for later use.
        self._log.debug(u'Discogs token {0}, secret {1}', token, secret)
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret

    def album_distance(self, items, album_info, mapping):
        """Returns the album distance.
        """
        return get_distance(data_source='Discogs',
                            info=album_info,
                            config=self.config)

    def track_distance(self, item, track_info):
        """Returns the track distance.
        """
        return get_distance(data_source='Discogs',
                            info=track_info,
                            config=self.config)

    def candidates(self, items, artist, album, va_likely, extra_tags=None):
        """Returns a list of AlbumInfo objects for discogs search results
        matching an album and artist (if not various).
        """
        if not self.discogs_client:
            return

        if va_likely:
            query = album
        else:
            query = '%s %s' % (artist, album)
        try:
            return self.get_albums(query)
        except DiscogsAPIError as e:
            self._log.debug(u'API Error: {0} (query: {1})', e, query)
            if e.status_code == 401:
                self.reset_auth()
                return self.candidates(items, artist, album, va_likely)
            else:
                return []
        except CONNECTION_ERRORS:
            self._log.debug(u'Connection error in album search', exc_info=True)
            return []

    def album_for_id(self, album_id):
        """Fetches an album by its Discogs ID and returns an AlbumInfo object
        or None if the album is not found.
        """
        if not self.discogs_client:
            return

        self._log.debug(u'Searching for release {0}', album_id)
        # Discogs-IDs are simple integers. We only look for those at the end
        # of an input string as to avoid confusion with other metadata plugins.
        # An optional bracket can follow the integer, as this is how discogs
        # displays the release ID on its webpage.
        match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
                          album_id)
        if not match:
            return None
        result = Release(self.discogs_client, {'id': int(match.group(2))})
        # Try to obtain title to verify that we indeed have a valid Release
        try:
            getattr(result, 'title')
        except DiscogsAPIError as e:
            if e.status_code != 404:
                self._log.debug(u'API Error: {0} (query: {1})', e,
                                result.data['resource_url'])
                if e.status_code == 401:
                    self.reset_auth()
                    return self.album_for_id(album_id)
            return None
        except CONNECTION_ERRORS:
            self._log.debug(u'Connection error in album lookup', exc_info=True)
            return None
        return self.get_album_info(result)

    def get_albums(self, query):
        """Returns a list of AlbumInfo objects for a discogs search query.
        """
        # Strip non-word characters from query. Things like "!" and "-" can
        # cause a query to return no results, even if they match the artist or
        # album title. Use `re.UNICODE` flag to avoid stripping non-english
        # word characters.
        # FIXME: Encode as ASCII to work around a bug:
        # https://github.com/beetbox/beets/issues/1051
        # When the library is fixed, we should encode as UTF-8.
        query = re.sub(r'(?u)\W+', ' ', query).encode('ascii', "replace")
        # Strip medium information from query, Things like "CD1" and "disk 1"
        # can also negate an otherwise positive result.
        query = re.sub(br'(?i)\b(CD|disc)\s*\d+', b'', query)

        self.request_start()
        try:
            releases = self.discogs_client.search(query,
                                                  type='release').page(1)
            self.request_finished()

        except CONNECTION_ERRORS:
            self._log.debug(u"Communication error while searching for {0!r}",
                            query,
                            exc_info=True)
            return []
        return [
            album for album in map(self.get_album_info, releases[:5]) if album
        ]

    def get_master_year(self, master_id):
        """Fetches a master release given its Discogs ID and returns its year
        or None if the master release is not found.
        """
        self._log.debug(u'Searching for master release {0}', master_id)
        result = Master(self.discogs_client, {'id': master_id})

        self.request_start()
        try:
            year = result.fetch('year')
            self.request_finished()
            return year
        except DiscogsAPIError as e:
            if e.status_code != 404:
                self._log.debug(u'API Error: {0} (query: {1})', e,
                                result.data['resource_url'])
                if e.status_code == 401:
                    self.reset_auth()
                    return self.get_master_year(master_id)
            return None
        except CONNECTION_ERRORS:
            self._log.debug(u'Connection error in master release lookup',
                            exc_info=True)
            return None

    def get_album_info(self, result):
        """Returns an AlbumInfo object for a discogs Release object.
        """
        # Explicitly reload the `Release` fields, as they might not be yet
        # present if the result is from a `discogs_client.search()`.
        if not result.data.get('artists'):
            result.refresh()

        # Sanity check for required fields. The list of required fields is
        # defined at Guideline 1.3.1.a, but in practice some releases might be
        # lacking some of these fields. This function expects at least:
        # `artists` (>0), `title`, `id`, `tracklist` (>0)
        # https://www.discogs.com/help/doc/submission-guidelines-general-rules
        if not all([
                result.data.get(k)
                for k in ['artists', 'title', 'id', 'tracklist']
        ]):
            self._log.warning(u"Release does not contain the required fields")
            return None

        artist, artist_id = MetadataSourcePlugin.get_artist(
            [a.data for a in result.artists])
        album = re.sub(r' +', ' ', result.title)
        album_id = result.data['id']
        # Use `.data` to access the tracklist directly instead of the
        # convenient `.tracklist` property, which will strip out useful artist
        # information and leave us with skeleton `Artist` objects that will
        # each make an API call just to get the same data back.
        tracks = self.get_tracks(result.data['tracklist'])

        # Extract information for the optional AlbumInfo fields, if possible.
        va = result.data['artists'][0].get('name', '').lower() == 'various'
        year = result.data.get('year')
        mediums = [t.medium for t in tracks]
        country = result.data.get('country')
        data_url = result.data.get('uri')
        style = self.format(result.data.get('styles'))
        genre = self.format(result.data.get('genres'))
        discogs_albumid = self.extract_release_id(result.data.get('uri'))

        # Extract information for the optional AlbumInfo fields that are
        # contained on nested discogs fields.
        albumtype = media = label = catalogno = labelid = None
        if result.data.get('formats'):
            albumtype = ', '.join(result.data['formats'][0].get(
                'descriptions', [])) or None
            media = result.data['formats'][0]['name']
        if result.data.get('labels'):
            label = result.data['labels'][0].get('name')
            catalogno = result.data['labels'][0].get('catno')
            labelid = result.data['labels'][0].get('id')

        # Additional cleanups (various artists name, catalog number, media).
        if va:
            artist = config['va_name'].as_str()
        if catalogno == 'none':
            catalogno = None
        # Explicitly set the `media` for the tracks, since it is expected by
        # `autotag.apply_metadata`, and set `medium_total`.
        for track in tracks:
            track.media = media
            track.medium_total = mediums.count(track.medium)
            # Discogs does not have track IDs. Invent our own IDs as proposed
            # in #2336.
            track.track_id = str(album_id) + "-" + track.track_alt

        # Retrieve master release id (returns None if there isn't one).
        master_id = result.data.get('master_id')
        # Assume `original_year` is equal to `year` for releases without
        # a master release, otherwise fetch the master release.
        original_year = self.get_master_year(master_id) if master_id else year

        return AlbumInfo(album=album,
                         album_id=album_id,
                         artist=artist,
                         artist_id=artist_id,
                         tracks=tracks,
                         albumtype=albumtype,
                         va=va,
                         year=year,
                         label=label,
                         mediums=len(set(mediums)),
                         releasegroup_id=master_id,
                         catalognum=catalogno,
                         country=country,
                         style=style,
                         genre=genre,
                         media=media,
                         original_year=original_year,
                         data_source='Discogs',
                         data_url=data_url,
                         discogs_albumid=discogs_albumid,
                         discogs_labelid=labelid,
                         discogs_artistid=artist_id)

    def format(self, classification):
        if classification:
            return self.config['separator'].as_str() \
                .join(sorted(classification))
        else:
            return None

    def extract_release_id(self, uri):
        if uri:
            return uri.split("/")[-1]
        else:
            return None

    def get_tracks(self, tracklist):
        """Returns a list of TrackInfo objects for a discogs tracklist.
        """
        try:
            clean_tracklist = self.coalesce_tracks(tracklist)
        except Exception as exc:
            # FIXME: this is an extra precaution for making sure there are no
            # side effects after #2222. It should be removed after further
            # testing.
            self._log.debug(u'{}', traceback.format_exc())
            self._log.error(u'uncaught exception in coalesce_tracks: {}', exc)
            clean_tracklist = tracklist
        tracks = []
        index_tracks = {}
        index = 0
        # Distinct works and intra-work divisions, as defined by index tracks.
        divisions, next_divisions = [], []
        for track in clean_tracklist:
            # Only real tracks have `position`. Otherwise, it's an index track.
            if track['position']:
                index += 1
                if next_divisions:
                    # End of a block of index tracks: update the current
                    # divisions.
                    divisions += next_divisions
                    del next_divisions[:]
                track_info = self.get_track_info(track, index, divisions)
                track_info.track_alt = track['position']
                tracks.append(track_info)
            else:
                next_divisions.append(track['title'])
                # We expect new levels of division at the beginning of the
                # tracklist (and possibly elsewhere).
                try:
                    divisions.pop()
                except IndexError:
                    pass
                index_tracks[index + 1] = track['title']

        # Fix up medium and medium_index for each track. Discogs position is
        # unreliable, but tracks are in order.
        medium = None
        medium_count, index_count, side_count = 0, 0, 0
        sides_per_medium = 1

        # If a medium has two sides (ie. vinyl or cassette), each pair of
        # consecutive sides should belong to the same medium.
        if all([track.medium is not None for track in tracks]):
            m = sorted(set([track.medium.lower() for track in tracks]))
            # If all track.medium are single consecutive letters, assume it is
            # a 2-sided medium.
            if ''.join(m) in ascii_lowercase:
                sides_per_medium = 2

        for track in tracks:
            # Handle special case where a different medium does not indicate a
            # new disc, when there is no medium_index and the ordinal of medium
            # is not sequential. For example, I, II, III, IV, V. Assume these
            # are the track index, not the medium.
            # side_count is the number of mediums or medium sides (in the case
            # of two-sided mediums) that were seen before.
            medium_is_index = track.medium and not track.medium_index and (
                len(track.medium) != 1 or
                # Not within standard incremental medium values (A, B, C, ...).
                ord(track.medium) - 64 != side_count + 1)

            if not medium_is_index and medium != track.medium:
                side_count += 1
                if sides_per_medium == 2:
                    if side_count % sides_per_medium:
                        # Two-sided medium changed. Reset index_count.
                        index_count = 0
                        medium_count += 1
                else:
                    # Medium changed. Reset index_count.
                    medium_count += 1
                    index_count = 0
                medium = track.medium

            index_count += 1
            medium_count = 1 if medium_count == 0 else medium_count
            track.medium, track.medium_index = medium_count, index_count

        # Get `disctitle` from Discogs index tracks. Assume that an index track
        # before the first track of each medium is a disc title.
        for track in tracks:
            if track.medium_index == 1:
                if track.index in index_tracks:
                    disctitle = index_tracks[track.index]
                else:
                    disctitle = None
            track.disctitle = disctitle

        return tracks

    def coalesce_tracks(self, raw_tracklist):
        """Pre-process a tracklist, merging subtracks into a single track. The
        title for the merged track is the one from the previous index track,
        if present; otherwise it is a combination of the subtracks titles.
        """
        def add_merged_subtracks(tracklist, subtracks):
            """Modify `tracklist` in place, merging a list of `subtracks` into
            a single track into `tracklist`."""
            # Calculate position based on first subtrack, without subindex.
            idx, medium_idx, sub_idx = \
                self.get_track_index(subtracks[0]['position'])
            position = '%s%s' % (idx or '', medium_idx or '')

            if tracklist and not tracklist[-1]['position']:
                # Assume the previous index track contains the track title.
                if sub_idx:
                    # "Convert" the track title to a real track, discarding the
                    # subtracks assuming they are logical divisions of a
                    # physical track (12.2.9 Subtracks).
                    tracklist[-1]['position'] = position
                else:
                    # Promote the subtracks to real tracks, discarding the
                    # index track, assuming the subtracks are physical tracks.
                    index_track = tracklist.pop()
                    # Fix artists when they are specified on the index track.
                    if index_track.get('artists'):
                        for subtrack in subtracks:
                            if not subtrack.get('artists'):
                                subtrack['artists'] = index_track['artists']
                    tracklist.extend(subtracks)
            else:
                # Merge the subtracks, pick a title, and append the new track.
                track = subtracks[0].copy()
                track['title'] = ' / '.join([t['title'] for t in subtracks])
                tracklist.append(track)

        # Pre-process the tracklist, trying to identify subtracks.
        subtracks = []
        tracklist = []
        prev_subindex = ''
        for track in raw_tracklist:
            # Regular subtrack (track with subindex).
            if track['position']:
                _, _, subindex = self.get_track_index(track['position'])
                if subindex:
                    if subindex.rjust(len(raw_tracklist)) > prev_subindex:
                        # Subtrack still part of the current main track.
                        subtracks.append(track)
                    else:
                        # Subtrack part of a new group (..., 1.3, *2.1*, ...).
                        add_merged_subtracks(tracklist, subtracks)
                        subtracks = [track]
                    prev_subindex = subindex.rjust(len(raw_tracklist))
                    continue

            # Index track with nested sub_tracks.
            if not track['position'] and 'sub_tracks' in track:
                # Append the index track, assuming it contains the track title.
                tracklist.append(track)
                add_merged_subtracks(tracklist, track['sub_tracks'])
                continue

            # Regular track or index track without nested sub_tracks.
            if subtracks:
                add_merged_subtracks(tracklist, subtracks)
                subtracks = []
                prev_subindex = ''
            tracklist.append(track)

        # Merge and add the remaining subtracks, if any.
        if subtracks:
            add_merged_subtracks(tracklist, subtracks)

        return tracklist

    def get_track_info(self, track, index, divisions):
        """Returns a TrackInfo object for a discogs track.
        """
        title = track['title']
        if self.config['index_tracks']:
            prefix = ', '.join(divisions)
            title = ': '.join([prefix, title])
        track_id = None
        medium, medium_index, _ = self.get_track_index(track['position'])
        artist, artist_id = MetadataSourcePlugin.get_artist(
            track.get('artists', []))
        length = self.get_track_length(track['duration'])
        return TrackInfo(title=title,
                         track_id=track_id,
                         artist=artist,
                         artist_id=artist_id,
                         length=length,
                         index=index,
                         medium=medium,
                         medium_index=medium_index)

    def get_track_index(self, position):
        """Returns the medium, medium index and subtrack index for a discogs
        track position."""
        # Match the standard Discogs positions (12.2.9), which can have several
        # forms (1, 1-1, A1, A1.1, A1a, ...).
        match = re.match(
            r'^(.*?)'  # medium: everything before medium_index.
            r'(\d*?)'  # medium_index: a number at the end of
            # `position`, except if followed by a subtrack
            # index.
            # subtrack_index: can only be matched if medium
            # or medium_index have been matched, and can be
            r'((?<=\w)\.[\w]+'  # - a dot followed by a string (A.1, 2.A)
            r'|(?<=\d)[A-Z]+'  # - a string that follows a number (1A, B2a)
            r')?'
            r'$',
            position.upper())

        if match:
            medium, index, subindex = match.groups()

            if subindex and subindex.startswith('.'):
                subindex = subindex[1:]
        else:
            self._log.debug(u'Invalid position: {0}', position)
            medium = index = subindex = None
        return medium or None, index or None, subindex or None

    def get_track_length(self, duration):
        """Returns the track length in seconds for a discogs duration.
        """
        try:
            length = time.strptime(duration, '%M:%S')
        except ValueError:
            return None
        return length.tm_min * 60 + length.tm_sec
Esempio n. 30
0
# Importing discogs_client from https://github.com/joalla/discogs_client
from discogs_client import Client

d = Client('ZorroDiscogClient/0.1',
           user_token='pKyILjZnXBGRToDMAcXdcGDpPtMTgNyzqRSVBBJO')


#   General search query
def search(query):
    results = d.search(query, format='vinyl')
    return results


#   Search by barcode
def searchbarcode(query):
    results = d.search(query, type='release', format='vinyl')
    return results


#   Searches by artist ID
def searchartistid(artistid):
    results = d.artist(artistid)
    print(results.name)


#   Searched by release id
def searchreleaseid(releaseid):
    results = d.release(releaseid)
    print(results.title)
    print(results.artists)
    print(results.year)
Esempio n. 31
0
class DiscogsPlugin(BeetsPlugin):

    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'apikey': 'rAzVUQYRaoFjeBjyWuWZ',
            'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy',
            'tokenfile': 'discogs_token.json',
            'source_weight': 0.5,
        })
        self.discogs_client = None
        self.register_listener('import_begin', self.setup)

    def setup(self):
        """Create the `discogs_client` field. Authenticate if necessary.
        """
        c_key = self.config['apikey'].get(unicode)
        c_secret = self.config['apisecret'].get(unicode)

        # Get the OAuth token from a file or log in.
        try:
            with open(self._tokenfile()) as f:
                tokendata = json.load(f)
        except IOError:
            # No token yet. Generate one.
            token, secret = self.authenticate(c_key, c_secret)
        else:
            token = tokendata['token']
            secret = tokendata['secret']

        self.discogs_client = Client(USER_AGENT, c_key, c_secret,
                                     token, secret)

    def _tokenfile(self):
        """Get the path to the JSON file for storing the OAuth token.
        """
        return self.config['tokenfile'].get(confit.Filename(in_app_dir=True))

    def authenticate(self, c_key, c_secret):
        # Get the link for the OAuth page.
        auth_client = Client(USER_AGENT, c_key, c_secret)
        _, _, url = auth_client.get_authorize_url()
        beets.ui.print_("To authenticate with Discogs, visit:")
        beets.ui.print_(url)

        # Ask for the code and validate it.
        code = beets.ui.input_("Enter the code:")
        try:
            token, secret = auth_client.get_access_token(code)
        except DiscogsAPIError:
            raise beets.ui.UserError('Discogs authorization failed')

        # Save the token for later use.
        log.debug('Discogs token {0}, secret {1}', token, secret)
        with open(self._tokenfile(), 'w') as f:
            json.dump({'token': token, 'secret': secret}, f)

        return token, secret

    def album_distance(self, items, album_info, mapping):
        """Returns the album distance.
        """
        dist = Distance()
        if album_info.data_source == 'Discogs':
            dist.add('source', self.config['source_weight'].as_number())
        return dist

    def candidates(self, items, artist, album, va_likely):
        """Returns a list of AlbumInfo objects for discogs search results
        matching an album and artist (if not various).
        """
        if not self.discogs_client:
            return

        if va_likely:
            query = album
        else:
            query = '%s %s' % (artist, album)
        try:
            return self.get_albums(query)
        except DiscogsAPIError as e:
            log.debug(u'Discogs API Error: {0} (query: {1})', e, query)
            return []
        except ConnectionError as e:
            log.debug(u'HTTP Connection Error: {0}', e)
            return []

    def album_for_id(self, album_id):
        """Fetches an album by its Discogs ID and returns an AlbumInfo object
        or None if the album is not found.
        """
        if not self.discogs_client:
            return

        log.debug(u'Searching Discogs for release {0}', album_id)
        # Discogs-IDs are simple integers. We only look for those at the end
        # of an input string as to avoid confusion with other metadata plugins.
        # An optional bracket can follow the integer, as this is how discogs
        # displays the release ID on its webpage.
        match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
                          album_id)
        if not match:
            return None
        result = Release(self.discogs_client, {'id': int(match.group(2))})
        # Try to obtain title to verify that we indeed have a valid Release
        try:
            getattr(result, 'title')
        except DiscogsAPIError as e:
            if e.message != '404 Not Found':
                log.debug(u'Discogs API Error: {0} (query: {1})',
                          e, result._uri)
            return None
        except ConnectionError as e:
            log.debug(u'HTTP Connection Error: {0}', e)
            return None
        return self.get_album_info(result)

    def get_albums(self, query):
        """Returns a list of AlbumInfo objects for a discogs search query.
        """
        # Strip non-word characters from query. Things like "!" and "-" can
        # cause a query to return no results, even if they match the artist or
        # album title. Use `re.UNICODE` flag to avoid stripping non-english
        # word characters.
        # TEMPORARY: Encode as ASCII to work around a bug:
        # https://github.com/sampsyo/beets/issues/1051
        # When the library is fixed, we should encode as UTF-8.
        query = re.sub(r'(?u)\W+', ' ', query).encode('ascii', "replace")
        # Strip medium information from query, Things like "CD1" and "disk 1"
        # can also negate an otherwise positive result.
        query = re.sub(r'(?i)\b(CD|disc)\s*\d+', '', query)
        releases = self.discogs_client.search(query, type='release').page(1)
        return [self.get_album_info(release) for release in releases[:5]]

    def get_album_info(self, result):
        """Returns an AlbumInfo object for a discogs Release object.
        """
        artist, artist_id = self.get_artist([a.data for a in result.artists])
        album = re.sub(r' +', ' ', result.title)
        album_id = result.data['id']
        # Use `.data` to access the tracklist directly instead of the
        # convenient `.tracklist` property, which will strip out useful artist
        # information and leave us with skeleton `Artist` objects that will
        # each make an API call just to get the same data back.
        tracks = self.get_tracks(result.data['tracklist'])
        albumtype = ', '.join(
            result.data['formats'][0].get('descriptions', [])) or None
        va = result.data['artists'][0]['name'].lower() == 'various'
        year = result.data['year']
        label = result.data['labels'][0]['name']
        mediums = len(set(t.medium for t in tracks))
        catalogno = result.data['labels'][0]['catno']
        if catalogno == 'none':
            catalogno = None
        country = result.data.get('country')
        media = result.data['formats'][0]['name']
        data_url = result.data['uri']
        return AlbumInfo(album, album_id, artist, artist_id, tracks, asin=None,
                         albumtype=albumtype, va=va, year=year, month=None,
                         day=None, label=label, mediums=mediums,
                         artist_sort=None, releasegroup_id=None,
                         catalognum=catalogno, script=None, language=None,
                         country=country, albumstatus=None, media=media,
                         albumdisambig=None, artist_credit=None,
                         original_year=None, original_month=None,
                         original_day=None, data_source='Discogs',
                         data_url=data_url)

    def get_artist(self, artists):
        """Returns an artist string (all artists) and an artist_id (the main
        artist) for a list of discogs album or track artists.
        """
        artist_id = None
        bits = []
        for i, artist in enumerate(artists):
            if not artist_id:
                artist_id = artist['id']
            name = artist['name']
            # Strip disambiguation number.
            name = re.sub(r' \(\d+\)$', '', name)
            # Move articles to the front.
            name = re.sub(r'(?i)^(.*?), (a|an|the)$', r'\2 \1', name)
            bits.append(name)
            if artist['join'] and i < len(artists) - 1:
                bits.append(artist['join'])
        artist = ' '.join(bits).replace(' ,', ',') or None
        return artist, artist_id

    def get_tracks(self, tracklist):
        """Returns a list of TrackInfo objects for a discogs tracklist.
        """
        tracks = []
        index_tracks = {}
        index = 0
        for track in tracklist:
            # Only real tracks have `position`. Otherwise, it's an index track.
            if track['position']:
                index += 1
                tracks.append(self.get_track_info(track, index))
            else:
                index_tracks[index + 1] = track['title']

        # Fix up medium and medium_index for each track. Discogs position is
        # unreliable, but tracks are in order.
        medium = None
        medium_count, index_count = 0, 0
        for track in tracks:
            # Handle special case where a different medium does not indicate a
            # new disc, when there is no medium_index and the ordinal of medium
            # is not sequential. For example, I, II, III, IV, V. Assume these
            # are the track index, not the medium.
            medium_is_index = track.medium and not track.medium_index and (
                len(track.medium) != 1 or
                ord(track.medium) - 64 != medium_count + 1
            )

            if not medium_is_index and medium != track.medium:
                # Increment medium_count and reset index_count when medium
                # changes.
                medium = track.medium
                medium_count += 1
                index_count = 0
            index_count += 1
            track.medium, track.medium_index = medium_count, index_count

        # Get `disctitle` from Discogs index tracks. Assume that an index track
        # before the first track of each medium is a disc title.
        for track in tracks:
            if track.medium_index == 1:
                if track.index in index_tracks:
                    disctitle = index_tracks[track.index]
                else:
                    disctitle = None
            track.disctitle = disctitle

        return tracks

    def get_track_info(self, track, index):
        """Returns a TrackInfo object for a discogs track.
        """
        title = track['title']
        track_id = None
        medium, medium_index = self.get_track_index(track['position'])
        artist, artist_id = self.get_artist(track.get('artists', []))
        length = self.get_track_length(track['duration'])
        return TrackInfo(title, track_id, artist, artist_id, length, index,
                         medium, medium_index, artist_sort=None,
                         disctitle=None, artist_credit=None)

    def get_track_index(self, position):
        """Returns the medium and medium index for a discogs track position.
        """
        # medium_index is a number at the end of position. medium is everything
        # else. E.g. (A)(1), (Side A, Track )(1), (A)(), ()(1), etc.
        match = re.match(r'^(.*?)(\d*)$', position.upper())
        if match:
            medium, index = match.groups()
        else:
            log.debug(u'Invalid Discogs position: {0}', position)
            medium = index = None
        return medium or None, index or None

    def get_track_length(self, duration):
        """Returns the track length in seconds for a discogs duration.
        """
        try:
            length = time.strptime(duration, '%M:%S')
        except ValueError:
            return None
        return length.tm_min * 60 + length.tm_sec
Esempio n. 32
0
class DiscogsPlugin(BeetsPlugin):

    def __init__(self):
        super(DiscogsPlugin, self).__init__()
        self.config.add({
            'source_weight': 0.5,
        })
        self.discogs_client = Client('beets/%s +http://beets.radbox.org/' %
                                     beets.__version__)

    def album_distance(self, items, album_info, mapping):
        """Returns the album distance.
        """
        dist = Distance()
        if album_info.data_source == 'Discogs':
            dist.add('source', self.config['source_weight'].as_number())
        return dist

    def candidates(self, items, artist, album, va_likely):
        """Returns a list of AlbumInfo objects for discogs search results
        matching an album and artist (if not various).
        """
        if va_likely:
            query = album
        else:
            query = '%s %s' % (artist, album)
        try:
            return self.get_albums(query)
        except DiscogsAPIError as e:
            log.debug('Discogs API Error: %s (query: %s' % (e, query))
            return []

    def album_for_id(self, album_id):
        """Fetches an album by its Discogs ID and returns an AlbumInfo object
        or None if the album is not found.
        """
        log.debug('Searching discogs for release %s' % str(album_id))
        # Discogs-IDs are simple integers. We only look for those at the end
        # of an input string as to avoid confusion with other metadata plugins.
        # An optional bracket can follow the integer, as this is how discogs
        # displays the release ID on its webpage.
        match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])',
                          album_id)
        if not match:
            return None
        result = Release(self.discogs_client, {'id': int(match.group(2))})
        # Try to obtain title to verify that we indeed have a valid Release
        try:
            getattr(result, 'title')
        except DiscogsAPIError as e:
            if e.message != '404 Not Found':
                log.debug('Discogs API Error: %s (query: %s)'
                          % (e, result._uri))
            return None
        return self.get_album_info(result)

    def get_albums(self, query):
        """Returns a list of AlbumInfo objects for a discogs search query.
        """
        # Strip non-word characters from query. Things like "!" and "-" can
        # cause a query to return no results, even if they match the artist or
        # album title. Use `re.UNICODE` flag to avoid stripping non-english
        # word characters.
        query = re.sub(r'(?u)\W+', ' ', query).encode('utf8')
        # Strip medium information from query, Things like "CD1" and "disk 1"
        # can also negate an otherwise positive result.
        query = re.sub(r'(?i)\b(CD|disc)\s*\d+', '', query)
        releases = self.discogs_client.search(query, type='release').page(1)
        return [self.get_album_info(release) for release in releases[:5]]

    def get_album_info(self, result):
        """Returns an AlbumInfo object for a discogs Release object.
        """
        artist, artist_id = self.get_artist([a.data for a in result.artists])
        album = re.sub(r' +', ' ', result.title)
        album_id = result.data['id']
        # Use `.data` to access the tracklist directly instead of the
        # convenient `.tracklist` property, which will strip out useful artist
        # information and leave us with skeleton `Artist` objects that will
        # each make an API call just to get the same data back.
        tracks = self.get_tracks(result.data['tracklist'])
        albumtype = ', '.join(
            result.data['formats'][0].get('descriptions', [])) or None
        va = result.data['artists'][0]['name'].lower() == 'various'
        year = result.data['year']
        label = result.data['labels'][0]['name']
        mediums = len(set(t.medium for t in tracks))
        catalogno = result.data['labels'][0]['catno']
        if catalogno == 'none':
            catalogno = None
        country = result.data.get('country')
        media = result.data['formats'][0]['name']
        data_url = result.data['uri']
        return AlbumInfo(album, album_id, artist, artist_id, tracks, asin=None,
                         albumtype=albumtype, va=va, year=year, month=None,
                         day=None, label=label, mediums=mediums,
                         artist_sort=None, releasegroup_id=None,
                         catalognum=catalogno, script=None, language=None,
                         country=country, albumstatus=None, media=media,
                         albumdisambig=None, artist_credit=None,
                         original_year=None, original_month=None,
                         original_day=None, data_source='Discogs',
                         data_url=data_url)

    def get_artist(self, artists):
        """Returns an artist string (all artists) and an artist_id (the main
        artist) for a list of discogs album or track artists.
        """
        artist_id = None
        bits = []
        for artist in artists:
            if not artist_id:
                artist_id = artist['id']
            name = artist['name']
            # Strip disambiguation number.
            name = re.sub(r' \(\d+\)$', '', name)
            # Move articles to the front.
            name = re.sub(r'(?i)^(.*?), (a|an|the)$', r'\2 \1', name)
            bits.append(name)
            if artist['join']:
                bits.append(artist['join'])
        artist = ' '.join(bits).replace(' ,', ',') or None
        return artist, artist_id

    def get_tracks(self, tracklist):
        """Returns a list of TrackInfo objects for a discogs tracklist.
        """
        tracks = []
        index_tracks = {}
        index = 0
        for track in tracklist:
            # Only real tracks have `position`. Otherwise, it's an index track.
            if track['position']:
                index += 1
                tracks.append(self.get_track_info(track, index))
            else:
                index_tracks[index + 1] = track['title']

        # Fix up medium and medium_index for each track. Discogs position is
        # unreliable, but tracks are in order.
        medium = None
        medium_count, index_count = 0, 0
        for track in tracks:
            # Handle special case where a different medium does not indicate a
            # new disc, when there is no medium_index and the ordinal of medium
            # is not sequential. For example, I, II, III, IV, V. Assume these
            # are the track index, not the medium.
            medium_is_index = track.medium and not track.medium_index and (
                len(track.medium) != 1 or
                ord(track.medium) - 64 != medium_count + 1
            )

            if not medium_is_index and medium != track.medium:
                # Increment medium_count and reset index_count when medium
                # changes.
                medium = track.medium
                medium_count += 1
                index_count = 0
            index_count += 1
            track.medium, track.medium_index = medium_count, index_count

        # Get `disctitle` from Discogs index tracks. Assume that an index track
        # before the first track of each medium is a disc title.
        for track in tracks:
            if track.medium_index == 1:
                if track.index in index_tracks:
                    disctitle = index_tracks[track.index]
                else:
                    disctitle = None
            track.disctitle = disctitle

        return tracks

    def get_track_info(self, track, index):
        """Returns a TrackInfo object for a discogs track.
        """
        title = track['title']
        track_id = None
        medium, medium_index = self.get_track_index(track['position'])
        artist, artist_id = self.get_artist(track.get('artists', []))
        length = self.get_track_length(track['duration'])
        return TrackInfo(title, track_id, artist, artist_id, length, index,
                         medium, medium_index, artist_sort=None,
                         disctitle=None, artist_credit=None)

    def get_track_index(self, position):
        """Returns the medium and medium index for a discogs track position.
        """
        # medium_index is a number at the end of position. medium is everything
        # else. E.g. (A)(1), (Side A, Track )(1), (A)(), ()(1), etc.
        match = re.match(r'^(.*?)(\d*)$', position.upper())
        if match:
            medium, index = match.groups()
        else:
            log.debug('Invalid discogs position: %s' % position)
            medium = index = None
        return medium or None, index or None

    def get_track_length(self, duration):
        """Returns the track length in seconds for a discogs duration.
        """
        try:
            length = time.strptime(duration, '%M:%S')
        except ValueError:
            return None
        return length.tm_min * 60 + length.tm_sec