From c2c617594fbcbee6583320cf34263fa76ea3d5ce Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Mon, 5 Sep 2022 09:03:02 +0200 Subject: [PATCH 01/15] Save Discogs Release ID via MusicBrainz - Enable fetching 'url-rels' from the MusicBrainz release endpoint. - A MusicBrainz release might contain a link to a Discogs release which we save to the discogs_albumid attribute of the info object. - For extraction of the release ID from the Discogs URL, we use the method provided in the id_extractors util module. --- beets/autotag/mb.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index f380cd033..c39f91c2d 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -29,6 +29,7 @@ from beets import util from beets import config from collections import Counter from urllib.parse import urljoin +from beets.util.id_extractors import extract_discogs_id_regex VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' @@ -70,7 +71,7 @@ log = logging.getLogger('beets') RELEASE_INCLUDES = ['artists', 'media', 'recordings', 'release-groups', 'labels', 'artist-credits', 'aliases', 'recording-level-rels', 'work-rels', - 'work-level-rels', 'artist-rels', 'isrcs'] + 'work-level-rels', 'artist-rels', 'isrcs', 'url-rels'] BROWSE_INCLUDES = ['artist-credits', 'work-rels', 'artist-rels', 'recording-rels', 'release-rels'] if "work-level-rels" in musicbrainzngs.VALID_BROWSE_INCLUDES['recording']: @@ -511,6 +512,15 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo: in sorted(genres.items(), key=lambda g: -g[1]) ) + # We might find URLs to external sources (Discogs, Spotify, ...) + if release.get('url-relation-list'): + discogs_url = None + for url in release['url-relation-list']: + if url['type'] == 'discogs': + log.debug('Found link to Discogs release via MusicBrainz') + discogs_url = url['target'] + info.discogs_albumid = extract_release_id_regex(discogs_url) + extra_albumdatas = plugins.send('mb_album_extract', data=release) for extra_albumdata in extra_albumdatas: info.update(extra_albumdata) From 36daf93828ec4f5d8345fa03e76ebe307bd162b8 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sun, 12 Mar 2023 16:28:29 -0400 Subject: [PATCH 02/15] Add instructions to install directly from Github. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b894daddc..5f46c8d0d 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ shockingly simple if you know a little Python. Install ------- -You can install beets by typing ``pip install beets``. +You can install beets by typing ``pip install beets`` or directly from github using ``python -m pip install git+https://github.com/beetbox/beets.git``. command. Beets has also been packaged in the `software repositories`_ of several distributions. Check out the `Getting Started`_ guide for more information. From 53911c74ddd457503af1b292245134f510f9c3b5 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 13 Mar 2023 11:33:45 -0400 Subject: [PATCH 03/15] Link to dev docs --- README.rst | 2 +- docs/faq.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5f46c8d0d..47eed37be 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ shockingly simple if you know a little Python. Install ------- -You can install beets by typing ``pip install beets`` or directly from github using ``python -m pip install git+https://github.com/beetbox/beets.git``. command. +You can install beets by typing ``pip install beets`` or directly from Github (see details [here](https://beets.readthedocs.io/en/stable/faq.html#run-the-latest-source-version-of-beets)). Beets has also been packaged in the `software repositories`_ of several distributions. Check out the `Getting Started`_ guide for more information. diff --git a/docs/faq.rst b/docs/faq.rst index 5e1c73666..b5a6cea0e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -137,6 +137,8 @@ it's helpful to run on the "bleeding edge". To run the latest source: ``pip uninstall beets``. 2. Install from source. Choose one of these methods: + - Directly from github using + ``python -m pip install git+https://github.com/beetbox/beets.git`` command. Depending on your system, you may need to use ``pip3`` and ``python3`` instead of ``pip`` and ``python`` respectively. - Use ``pip`` to install the latest snapshot tarball. Type: ``pip install https://github.com/beetbox/beets/tarball/master`` - Grab the source using git. First, clone the repository: From 05cdecfb7cdb1e8d5d7387ab5a7f451b5919f8d0 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 14 Mar 2023 20:39:47 -0400 Subject: [PATCH 04/15] Add more examples to convert plugin --- docs/plugins/convert.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 7adb94079..4aed7c90a 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -94,9 +94,9 @@ file. The available options are: output. Note that this does not guarantee that all converted files will have a lower bitrate---that depends on the encoder and its configuration. Default: none. -- **no_convert**: Does not transcode items matching provided query string - (see :doc:`/reference/query`). (i.e. ``format:AAC, format:WMA`` or - ``path::\.(m4a|wma)$``) +- **no_convert**: Does not transcode items matching the query string provided + (see :doc:`/reference/query`). For example, to not convert AAC or WMA formats, you can use ``format:AAC, format:WMA`` or + ``path::\.(m4a|wma)$``. If you only want to transcode WMA format, you can use the query term negation to not convert all the other formats except wma, e.g., ``^path::\.(wma)$``. - **never_convert_lossy_files**: Cross-conversions between lossy codecs---such as mp3, ogg vorbis, etc.---makes little sense as they will decrease quality even further. If set to ``yes``, lossy files are always copied. From 5f87371229465a79713a12f5917d4e2bf3fcae9b Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Tue, 14 Mar 2023 20:57:34 -0400 Subject: [PATCH 05/15] rephrase --- docs/plugins/convert.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/convert.rst b/docs/plugins/convert.rst index 4aed7c90a..622a8f2cd 100644 --- a/docs/plugins/convert.rst +++ b/docs/plugins/convert.rst @@ -96,7 +96,7 @@ file. The available options are: Default: none. - **no_convert**: Does not transcode items matching the query string provided (see :doc:`/reference/query`). For example, to not convert AAC or WMA formats, you can use ``format:AAC, format:WMA`` or - ``path::\.(m4a|wma)$``. If you only want to transcode WMA format, you can use the query term negation to not convert all the other formats except wma, e.g., ``^path::\.(wma)$``. + ``path::\.(m4a|wma)$``. If you only want to transcode WMA format, you can use a negative query, e.g., ``^path::\.(wma)$``, to not convert any other format except WMA. - **never_convert_lossy_files**: Cross-conversions between lossy codecs---such as mp3, ogg vorbis, etc.---makes little sense as they will decrease quality even further. If set to ``yes``, lossy files are always copied. From 6d0e5ba8cabaab8ef7f0b142162d69082e08b78a Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Fri, 17 Mar 2023 11:06:46 -0400 Subject: [PATCH 06/15] Change to GitHub --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index b5a6cea0e..e7f5cc600 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -137,7 +137,7 @@ it's helpful to run on the "bleeding edge". To run the latest source: ``pip uninstall beets``. 2. Install from source. Choose one of these methods: - - Directly from github using + - Directly from GitHub using ``python -m pip install git+https://github.com/beetbox/beets.git`` command. Depending on your system, you may need to use ``pip3`` and ``python3`` instead of ``pip`` and ``python`` respectively. - Use ``pip`` to install the latest snapshot tarball. Type: ``pip install https://github.com/beetbox/beets/tarball/master`` From 9f4b43c54076c9195974ccb79ac57ab313e0bf08 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Sat, 18 Mar 2023 10:56:16 -0400 Subject: [PATCH 07/15] Point to latest docs --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 47eed37be..62fbfc298 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ shockingly simple if you know a little Python. Install ------- -You can install beets by typing ``pip install beets`` or directly from Github (see details [here](https://beets.readthedocs.io/en/stable/faq.html#run-the-latest-source-version-of-beets)). +You can install beets by typing ``pip install beets`` or directly from Github (see details [here](https://beets.readthedocs.io/en/latest/faq.html#run-the-latest-source-version-of-beets)). Beets has also been packaged in the `software repositories`_ of several distributions. Check out the `Getting Started`_ guide for more information. From 5231037591959a2f088c0474afacd45e06737bf0 Mon Sep 17 00:00:00 2001 From: Alok Saboo Date: Mon, 20 Mar 2023 17:32:45 -0400 Subject: [PATCH 08/15] Add JioSaavn as a tag source --- docs/plugins/index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index ea13d2feb..c9e008023 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -458,6 +458,9 @@ Here are a few of the plugins written by the beets community: Lets you perform regex replacements on incoming metadata. +`beets-jiosaavn`_ + Adds JioSaavn.com as a tagger data source.. + `beets-mosaic`_ Generates a montage of a mosaic from cover art. @@ -522,6 +525,7 @@ Here are a few of the plugins written by the beets community: .. _beets-usertag: https://github.com/igordertigor/beets-usertag .. _beets-popularity: https://github.com/abba23/beets-popularity .. _beets-plexsync: https://github.com/arsaboo/beets-plexsync +.. _beets-jiosaavn: https://github.com/arsaboo/beets-jiosaavn .. _beets-ydl: https://github.com/vmassuchetto/beets-ydl .. _beet-summarize: https://github.com/steven-murray/beet-summarize .. _beets-mosaic: https://github.com/SusannaMaria/beets-mosaic From 6f2c31926e7734c5b19cc28fe75bf678f5c1ac11 Mon Sep 17 00:00:00 2001 From: J0J0 T Date: Sun, 25 Sep 2022 08:35:19 +0200 Subject: [PATCH 09/15] Fetch more external IDs via MusicBrainz Add retrieval and ID extraction of Bandcamp, Spotify, Deezer and Beatport URLs while retrieving a MusicBrainz release. --- beets/autotag/mb.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index c39f91c2d..0daa6c205 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -29,7 +29,9 @@ from beets import util from beets import config from collections import Counter from urllib.parse import urljoin -from beets.util.id_extractors import extract_discogs_id_regex +from beets.util.id_extractors import extract_discogs_id_regex, \ + spotify_id_regex, deezer_id_regex, beatport_id_regex +from beets.plugins import MetadataSourcePlugin VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' @@ -512,14 +514,41 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo: in sorted(genres.items(), key=lambda g: -g[1]) ) - # We might find URLs to external sources (Discogs, Spotify, ...) + # We might find links to external sources (Discogs, Bandcamp, ...) if release.get('url-relation-list'): - discogs_url = None + discogs_url, bandcamp_url, spotify_url = None, None, None + deezer_url, beatport_url = None, None + for url in release['url-relation-list']: if url['type'] == 'discogs': log.debug('Found link to Discogs release via MusicBrainz') discogs_url = url['target'] - info.discogs_albumid = extract_release_id_regex(discogs_url) + if 'bandcamp.com' in url['target']: + log.debug('Found link to Bandcamp release via MusicBrainz') + bandcamp_url = url['target'] + if 'spotify.com' in url['target']: + log.debug('Found link to Spotify album via MusicBrainz') + spotify_url = url['target'] + if 'deezer.com' in url['target']: + log.debug('Found link to Deezer album via MusicBrainz') + deezer_url = url['target'] + if 'beatport.com' in url['target']: + log.debug('Found link to Beatport release via MusicBrainz') + beatport_url = url['target'] + + if discogs_url: + info.discogs_albumid = extract_discogs_id_regex(discogs_url) + if bandcamp_url: + info.bandcamp_album_id = bandcamp_url + if spotify_url: + info.spotify_album_id = MetadataSourcePlugin._get_id( + 'album', spotify_url, spotify_id_regex) + if deezer_url: + info.deezer_album_id = MetadataSourcePlugin._get_id( + 'album', deezer_url, deezer_id_regex) + if beatport_url: + info.beatport_album_id = MetadataSourcePlugin._get_id( + 'album', beatport_url, beatport_id_regex) extra_albumdatas = plugins.send('mb_album_extract', data=release) for extra_albumdata in extra_albumdatas: From fd4cecc29e24da70d50fb576ae3eadcda0353da8 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 12 Mar 2023 15:56:05 +0100 Subject: [PATCH 10/15] Make external IDs fetching via MB configurable - By default no external URLs will be looked for in the 'url-relation-list' of the releae dictionary. - Enabling is possible per each external service. --- beets/autotag/mb.py | 26 ++++++++++++++++++++------ beets/config_default.yaml | 6 ++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/beets/autotag/mb.py b/beets/autotag/mb.py index 0daa6c205..cee2bdfd9 100644 --- a/beets/autotag/mb.py +++ b/beets/autotag/mb.py @@ -515,24 +515,38 @@ def album_info(release: Dict) -> beets.autotag.hooks.AlbumInfo: ) # We might find links to external sources (Discogs, Bandcamp, ...) - if release.get('url-relation-list'): + if (any(config['musicbrainz']['external_ids'].get().values()) + and release.get('url-relation-list')): discogs_url, bandcamp_url, spotify_url = None, None, None deezer_url, beatport_url = None, None + fetch_discogs, fetch_bandcamp, fetch_spotify = False, False, False + fetch_deezer, fetch_beatport = False, False + + if config['musicbrainz']['external_ids']['discogs'].get(): + fetch_discogs = True + if config['musicbrainz']['external_ids']['bandcamp'].get(): + fetch_bandcamp = True + if config['musicbrainz']['external_ids']['spotify'].get(): + fetch_spotify = True + if config['musicbrainz']['external_ids']['deezer'].get(): + fetch_deezer = True + if config['musicbrainz']['external_ids']['beatport'].get(): + fetch_beatport = True for url in release['url-relation-list']: - if url['type'] == 'discogs': + if fetch_discogs and url['type'] == 'discogs': log.debug('Found link to Discogs release via MusicBrainz') discogs_url = url['target'] - if 'bandcamp.com' in url['target']: + if fetch_bandcamp and 'bandcamp.com' in url['target']: log.debug('Found link to Bandcamp release via MusicBrainz') bandcamp_url = url['target'] - if 'spotify.com' in url['target']: + if fetch_spotify and 'spotify.com' in url['target']: log.debug('Found link to Spotify album via MusicBrainz') spotify_url = url['target'] - if 'deezer.com' in url['target']: + if fetch_deezer and 'deezer.com' in url['target']: log.debug('Found link to Deezer album via MusicBrainz') deezer_url = url['target'] - if 'beatport.com' in url['target']: + if fetch_beatport and 'beatport.com' in url['target']: log.debug('Found link to Beatport release via MusicBrainz') beatport_url = url['target'] diff --git a/beets/config_default.yaml b/beets/config_default.yaml index 2798b3872..6dcadccb2 100644 --- a/beets/config_default.yaml +++ b/beets/config_default.yaml @@ -128,6 +128,12 @@ musicbrainz: searchlimit: 5 extra_tags: [] genres: no + external_ids: + discogs: no + bandcamp: no + spotify: no + deezer: no + beatport: no match: strong_rec_thresh: 0.04 From 6acee803f7e74ccc656f33e79c7527f07a31c1ca Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Thu, 3 Nov 2022 20:37:54 +0100 Subject: [PATCH 11/15] Exclude ID's when preserving flexible attributes - Bandcamp, Spotify, Deezer and Beatport ID's are saved in the library as flexible attributes. - On _reimports_ the method importer.ImportTask.reimport_metadata() takes care of preserving existing values for flexible attributes instead of applying new (and potentially empty) values. - In this case we don't want this behaviour and need to make sure that new values are applied. Therefore we check whether such ID's of metadata services are present in the reimported items and exclude them in reimport_metadata(). --- beets/importer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/beets/importer.py b/beets/importer.py index c0319fc96..feebadc09 100644 --- a/beets/importer.py +++ b/beets/importer.py @@ -839,6 +839,19 @@ class ImportTask(BaseImportTask): dup_item.id, displayable_path(item.path) ) + # We exclude certain flexible attributes from the preserving + # process since they might have been fetched from MusicBrainz + # and been set in beets.autotag.apply_metadata(). + # discogs_albumid might also have been set but is not a + # flexible attribute, thus no exclude is required. + if item.get('bandcamp_album_id'): + dup_item.bandcamp_album_id = item.bandcamp_album_id + if item.get('spotify_album_id'): + dup_item.spotify_album_id = item.spotify_album_id + if item.get('deezer_album_id'): + dup_item.deezer_album_id = item.deezer_album_id + if item.get('beatport_album_id'): + dup_item.beatport_album_id = item.beatport_album_id item.update(dup_item._values_flex) log.debug( 'Reimported item flexible attributes {0} ' From 348a9fbf13a4ca144b97d4c8f9b56f78c81d812f Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Fri, 4 Nov 2022 16:51:40 +0100 Subject: [PATCH 12/15] Changelog for #4708 --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9071f8831..435c85709 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -63,6 +63,11 @@ New features: * :doc:`/plugins/fromfilename`: Add debug log messages that inform when the plugin replaced bad (missing) artist, title or tracknumber metadata. :bug:`4561` :bug:`4600` +* :ref:`musicbrainz-config`: MusicBrainz release pages often link to related + metadata sources like Discogs, Bandcamp, Spotify, Deezer and Beatport. When + enabled via the :ref:`musicbrainz.external_ids` options, release ID's will be + extracted from those URL's and imported to the library. + :bug:`4220` Bug fixes: From 2e5394246fafcc1a41b43fb75c24c47a1b95620c Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Fri, 4 Nov 2022 17:51:27 +0100 Subject: [PATCH 13/15] Docs for #4708 --- docs/plugins/index.rst | 2 ++ docs/reference/config.rst | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst index ea13d2feb..4256ad690 100644 --- a/docs/plugins/index.rst +++ b/docs/plugins/index.rst @@ -129,6 +129,8 @@ following to your configuration:: web zero +.. _autotagger_extensions: + Autotagger Extensions --------------------- diff --git a/docs/reference/config.rst b/docs/reference/config.rst index b6fa8fea6..f162c6762 100644 --- a/docs/reference/config.rst +++ b/docs/reference/config.rst @@ -842,6 +842,32 @@ release and the release-group on MusicBrainz, separated by "; " and sorted by the total number of votes. Default: ``no`` +.. _musicbrainz.external_ids: + +external_ids +~~~~~~~~~~~~ + +Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz +importer to look for links to related metadata sources. If such a link is +available the release ID will be extracted from the URL provided and imported +to the beets library. + + musicbrainz: + external_ids: + discogs: yes + spotify: yes + bandcamp: yes + beatport: yes + deezer: yes + + +The library fields of the corresponding :ref:`autotagger_extensions` are used +to save the data (``discogs_albumid``, ``bandcamp_album_id``, +``spotify_album_id``, ``beatport_album_id``, ``deezer_album_id``). On +re-imports existing data will be overwritten. + +The default of all options is ``no``. + .. _match-config: Autotagger Matching Options From 5bf4e3d92f5b4010a19cd3e8222fbf28b7b844e0 Mon Sep 17 00:00:00 2001 From: J0J0 Todos Date: Sun, 26 Mar 2023 13:10:28 +0200 Subject: [PATCH 14/15] Dedicated flex attrs for Deezer, Beatport album ID - Similar to what the Spotify plugin does, on imports we save to a field `..._album_id` (spotify_album_id, deezer_album_id, beatport_album_id) - It would be good to submit such a change to the 3rd-party plugins beetcamp and beatport4 as well (beatport_album_id, bandcamp_album_id). - We might need to investigate why none of these flex attr fields get populated to the beets album level (`beet info -a`, album_attributes db table), it is only available at the item level (`beet info`, item_attributes db table). This should be tackled in a future issue/PR. --- beetsplug/beatport.py | 1 + beetsplug/deezer.py | 1 + 2 files changed, 2 insertions(+) diff --git a/beetsplug/beatport.py b/beetsplug/beatport.py index eabf5dc31..bede8071d 100644 --- a/beetsplug/beatport.py +++ b/beetsplug/beatport.py @@ -432,6 +432,7 @@ class BeatportPlugin(BeetsPlugin): tracks = [self._get_track_info(x) for x in release.tracks] return AlbumInfo(album=release.name, album_id=release.beatport_id, + beatport_album_id=release.beatport_id, artist=artist, artist_id=artist_id, tracks=tracks, albumtype=release.category, va=va, year=release.release_date.year, diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index 86c182c39..77f7edc85 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -98,6 +98,7 @@ class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): return AlbumInfo( album=album_data['title'], album_id=deezer_id, + deezer_album_id=deezer_id, artist=artist, artist_credit=self.get_artist([album_data['artist']])[0], artist_id=artist_id, From 4e519b200dd4f02746af40c501c84218d9ad9dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Mon, 27 Mar 2023 02:27:32 +0100 Subject: [PATCH 15/15] Update beetcamp URL in id_extractors --- beets/util/id_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beets/util/id_extractors.py b/beets/util/id_extractors.py index b1020e78c..93fc2056c 100644 --- a/beets/util/id_extractors.py +++ b/beets/util/id_extractors.py @@ -35,7 +35,7 @@ beatport_id_regex = { # A note on Bandcamp: There is no such thing as a Bandcamp album or artist ID, # the URL can be used as the identifier. The Bandcamp metadata source plugin -# works that way - https://github.com/unrblt/beets-bandcamp. Bandcamp album +# works that way - https://github.com/snejus/beetcamp. Bandcamp album # URLs usually look like: https://nameofartist.bandcamp.com/album/nameofalbum