From c97d22e0f03ccfe8e911f2a3e2149a46b69cd92c Mon Sep 17 00:00:00 2001 From: Volker Date: Sat, 27 Dec 2025 17:06:31 +0100 Subject: [PATCH 1/4] version bump 1.1.2 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8cfbc90..867e524 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.1 \ No newline at end of file +1.2.0 \ No newline at end of file From 6efa6617d389be735aeaf45ee1a60b3594f221ee Mon Sep 17 00:00:00 2001 From: Volker Date: Sat, 27 Dec 2025 17:09:31 +0100 Subject: [PATCH 2/4] fix typo: mbz is the correct string --- Dockerfile | 6 +++--- README.md | 2 +- mbz_rss_service/__init__.py | 1 + {mzb_rss_service => mbz_rss_service}/config.py | 4 ++-- {mzb_rss_service => mbz_rss_service}/main.py | 2 +- {mzb_rss_service => mbz_rss_service}/templates/base.html | 0 {mzb_rss_service => mbz_rss_service}/templates/index.html | 0 {mzb_rss_service => mbz_rss_service}/templates/opml.xml | 0 .../templates/settings.html | 0 {mzb_rss_service => mbz_rss_service}/tests/requirements.txt | 0 mzb_rss_service/__init__.py | 1 - 11 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 mbz_rss_service/__init__.py rename {mzb_rss_service => mbz_rss_service}/config.py (98%) rename {mzb_rss_service => mbz_rss_service}/main.py (99%) rename {mzb_rss_service => mbz_rss_service}/templates/base.html (100%) rename {mzb_rss_service => mbz_rss_service}/templates/index.html (100%) rename {mzb_rss_service => mbz_rss_service}/templates/opml.xml (100%) rename {mzb_rss_service => mbz_rss_service}/templates/settings.html (100%) rename {mzb_rss_service => mbz_rss_service}/tests/requirements.txt (100%) delete mode 100644 mzb_rss_service/__init__.py diff --git a/Dockerfile b/Dockerfile index 083dd85..d1bd0cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ RUN mkdir -p /var/mbz-rss-feeder/cache /var/mbz-rss-feeder/log /var/mbz-rss-feed # Test stage FROM builder AS test -COPY mzb_rss_service/tests/requirements.txt . +COPY mbz_rss_service/tests/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python -m pytest"] @@ -35,8 +35,8 @@ CMD ["python -m pytest"] FROM base AS final COPY --from=builder /usr/local/lib/python3.10/site-packages /usr/local/lib/python3.10/site-packages COPY --from=builder /usr/local/bin /usr/local/bin -COPY mzb_rss_service mzb_rss_service +COPY mbz_rss_service mbz_rss_service COPY VERSION . EXPOSE 8080 -CMD ["gunicorn", "--bind", "0.0.0.0:8080", "mzb_rss_service.main:app"] +CMD ["gunicorn", "--bind", "0.0.0.0:8080", "mbz_rss_service.main:app"] diff --git a/README.md b/README.md index 3ce2055..3bdc921 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ This is a local container running with data stored in ./local/testdata -p 8080:8080 -e LOG_LEVEL=debug --rm --name mbz-rss\ -v ./local/testdata/:/var/mbz-rss-feeder \ --env-file ./local/testdata/.env \ - docker.io/library/mzb-rss-feeder:latest` + docker.io/library/mbz-rss-feeder:latest` After startup the server can be accessed through http://docker-host:8080. diff --git a/mbz_rss_service/__init__.py b/mbz_rss_service/__init__.py new file mode 100644 index 0000000..eb13bc3 --- /dev/null +++ b/mbz_rss_service/__init__.py @@ -0,0 +1 @@ +# mbz_rss_service package diff --git a/mzb_rss_service/config.py b/mbz_rss_service/config.py similarity index 98% rename from mzb_rss_service/config.py rename to mbz_rss_service/config.py index 377ef52..c29cac0 100644 --- a/mzb_rss_service/config.py +++ b/mbz_rss_service/config.py @@ -11,8 +11,8 @@ logger = logging.getLogger(__name__) FEEDS_FILE_PATH = os.path.expandvars(os.environ.get('FEEDS_FILE_PATH', '/var/mbz-rss-feeder/feeds.yml')) CONFIG_FILE_PATH = os.path.expandvars(os.environ.get('CONFIG_FILE_PATH', '/var/mbz-rss-feeder/etc/mbz-rss-feeder.yml')) CACHE_DIR = os.path.expandvars(os.environ.get('CACHE_DIR', '/var/mbz-rss-feeder/cache')) -MB_APP_NAME = os.path.expandvars(os.environ.get('MB_APP_NAME', 'mzb-rss-service')) -MB_VERSION = os.path.expandvars(os.environ.get('MB_VERSION', '0.1.0')) +MB_APP_NAME = os.path.expandvars(os.environ.get('MB_APP_NAME', 'mbz-rss-service')) +MB_VERSION = os.path.expandvars(os.environ.get('MB_VERSION', '1')) MB_CONTACT = os.path.expandvars(os.environ.get('MB_CONTACT', 'someone@somewhere.com')) # file system dir check diff --git a/mzb_rss_service/main.py b/mbz_rss_service/main.py similarity index 99% rename from mzb_rss_service/main.py rename to mbz_rss_service/main.py index 3e7c3fa..dedbca8 100644 --- a/mzb_rss_service/main.py +++ b/mbz_rss_service/main.py @@ -55,7 +55,7 @@ for handler in werkzeug_logger.handlers: logger = logging.getLogger(__name__) -logger.info(f"Starting mzb-rss-service v{config.VERSION} with the following configuration:") +logger.info(f"Starting mbz-rss-service v{config.VERSION} with the following configuration:") logger.info(f" - Log Level: {log_level_str}") logger.info(f" - Log File: {log_file}") logger.info(f" - Feeds File: {config.FEEDS_FILE_PATH}") diff --git a/mzb_rss_service/templates/base.html b/mbz_rss_service/templates/base.html similarity index 100% rename from mzb_rss_service/templates/base.html rename to mbz_rss_service/templates/base.html diff --git a/mzb_rss_service/templates/index.html b/mbz_rss_service/templates/index.html similarity index 100% rename from mzb_rss_service/templates/index.html rename to mbz_rss_service/templates/index.html diff --git a/mzb_rss_service/templates/opml.xml b/mbz_rss_service/templates/opml.xml similarity index 100% rename from mzb_rss_service/templates/opml.xml rename to mbz_rss_service/templates/opml.xml diff --git a/mzb_rss_service/templates/settings.html b/mbz_rss_service/templates/settings.html similarity index 100% rename from mzb_rss_service/templates/settings.html rename to mbz_rss_service/templates/settings.html diff --git a/mzb_rss_service/tests/requirements.txt b/mbz_rss_service/tests/requirements.txt similarity index 100% rename from mzb_rss_service/tests/requirements.txt rename to mbz_rss_service/tests/requirements.txt diff --git a/mzb_rss_service/__init__.py b/mzb_rss_service/__init__.py deleted file mode 100644 index 89cb37b..0000000 --- a/mzb_rss_service/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# mzb_rss_service package From 93eef59aa8be8eff041bedb195947df17d0af441 Mon Sep 17 00:00:00 2001 From: Volker Date: Sat, 27 Dec 2025 17:11:17 +0100 Subject: [PATCH 3/4] add related links artist links to feed page (/feed/:ID) release links to feed xml (release description) --- mbz_rss_service/config.py | 10 +++- mbz_rss_service/main.py | 11 ++-- .../musicbrainz.py | 56 ++++++++++++++++++- .../templates/feed.html | 5 ++ .../templates/feed.xml | 11 +++- 5 files changed, 82 insertions(+), 11 deletions(-) rename {mzb_rss_service => mbz_rss_service}/musicbrainz.py (63%) rename {mzb_rss_service => mbz_rss_service}/templates/feed.html (91%) rename {mzb_rss_service => mbz_rss_service}/templates/feed.xml (72%) diff --git a/mbz_rss_service/config.py b/mbz_rss_service/config.py index c29cac0..c9efed6 100644 --- a/mbz_rss_service/config.py +++ b/mbz_rss_service/config.py @@ -68,10 +68,13 @@ class Config: backup_path = f"{file_path}.{datetime.now().strftime('%Y%m%d%H%M%S')}.bak" shutil.copy(file_path, backup_path) + logger.debug(f"Saving yaml to {file_path}") os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w') as f: yaml.dump(data, f, default_flow_style=False, sort_keys=False) + logger.debug(f"Saved yaml to {file_path}") + @property def feeds(self): return self._feeds_data.get('feeds', []) @@ -105,13 +108,16 @@ class Config: self._feeds_data['feeds'] = [feed for feed in self.feeds if feed['id'] != feed_id] self.save_feeds() - def add_artist_to_feed(self, feed_id, artist_id, artist_name): + def add_artist_to_feed(self, feed_id, artist_id, artist_name, links = None): for feed in self.feeds: if feed['id'] == feed_id: if 'artists' not in feed: feed['artists'] = [] if not any(a['id'] == artist_id for a in feed['artists']): - feed['artists'].append({'id': artist_id, 'name': artist_name}) + artist_data = {'id': artist_id, 'name': artist_name} + if links: + artist_data['links'] = links + feed['artists'].append(artist_data) feed['updated_at'] = datetime.now(timezone.utc).isoformat() self.save_feeds() break diff --git a/mbz_rss_service/main.py b/mbz_rss_service/main.py index dedbca8..aa1e53b 100644 --- a/mbz_rss_service/main.py +++ b/mbz_rss_service/main.py @@ -9,6 +9,8 @@ import os import sys from . import musicbrainz +XML_CONTENT_TYPE = "application/xml" + log_level_str = os.environ.get('LOG_LEVEL', 'INFO').upper() log_level = getattr(logging, log_level_str, logging.INFO) log_file = os.path.expandvars(os.environ.get('LOG_FILE', '/var/mbz-rss-feeder/log/mbz-rss-feeder.log')) @@ -154,7 +156,7 @@ def _check_cache(feed_id): try: with open(cache_file, 'r') as f: logger.debug(f"Serving cached feed for {feed_id}") - return f.read(), {"Content-Type": "application/xml"} + return f.read(), {"Content-Type": XML_CONTENT_TYPE} except IOError as e: logger.warning(f"Could not read cache file {cache_file}: {e}") @@ -195,7 +197,8 @@ def add_artist(feed_id): artist_name = request.form.get('artist_name') logger.debug(f"Request to add artist '{artist_name}' ({artist_id}) to feed {feed_id}") if artist_id and artist_name: - config.add_artist_to_feed(feed_id, artist_id, artist_name) + meta = musicbrainz.get_artist_meta_by_id(artist_id) + config.add_artist_to_feed(feed_id, artist_id, artist_name, meta['links']) return redirect(url_for('edit_feed', feed_id=feed_id)) @app.route("/feed//artist//remove", methods=["POST"]) @@ -228,7 +231,7 @@ def opml(): logger.debug("Request for OPML file") feeds = config.feeds opml_content = render_template('opml.xml', feeds=feeds) - return opml_content, {"Content-Type": "application/xml"} + return opml_content, {"Content-Type": XML_CONTENT_TYPE} @app.route('/feed/') def get_feed_rss(feed_id): @@ -277,7 +280,7 @@ def get_feed_rss(feed_id): except IOError as e: logger.warning(f"Could not write feed '{feed_data['name']}' to cache file {cache_file}: {e}") - return rss_content, {"Content-Type": "application/xml"} + return rss_content, {"Content-Type": XML_CONTENT_TYPE} @app.route("/health") def health_check(): diff --git a/mzb_rss_service/musicbrainz.py b/mbz_rss_service/musicbrainz.py similarity index 63% rename from mzb_rss_service/musicbrainz.py rename to mbz_rss_service/musicbrainz.py index 03e920c..3dff102 100644 --- a/mzb_rss_service/musicbrainz.py +++ b/mbz_rss_service/musicbrainz.py @@ -1,6 +1,6 @@ +import re import musicbrainzngs import logging -import os from .config import config from datetime import datetime from email.utils import formatdate @@ -62,6 +62,10 @@ def _process_release(release, artist_id): release_date = release.get('date', 'Unknown') _, pub_date = _parse_release_date(release_date, release['title']) + links = {} + if 'url-relation-list' in release: + links = get_artist_relation_links(release['url-relation-list']) + return { 'artist': { 'name': config.get_artist_name(artist_id), @@ -72,8 +76,54 @@ def _process_release(release, artist_id): 'title': release['title'], 'date': release_date, 'pub_date': pub_date, - 'release-group': release.get('release-group', {}) + 'release-group': release.get('release-group', {}), + 'links': links, } +def get_artist_meta_by_id(artist_id): + """get additional meta data about an artist""" + meta = { + 'links': {} + } + try: + logger.debug(f"Fetching artist {artist_id} meta data") + # Fetch artist data including URL relations + result = musicbrainzngs.get_artist_by_id(artist_id, includes=["url-rels"]) + artist_data = result.get('artist', {}) + + if 'url-relation-list' in artist_data: + meta['links'] = get_artist_relation_links(artist_data['url-relation-list']) + else: + meta['links'] = None + + return meta + + except musicbrainzngs.WebServiceError as exc: + logger.error(f"Error fetching artist {artist_id} meta data from MusicBrainz: {exc}") + + return meta + +def get_artist_relation_links(relationlist): + """Parse and extract named links from a MusicBrainz URL relation list.""" + links = {} + for rel in relationlist: + target = rel.get('target') + if not target: + continue + if re.match(r'^https://.*imdb\.com', target): + links['IMDb'] = target + elif re.match(r'^https://music\.apple\.com', target): + links['apple'] = target + elif re.match(r'^https://music\.amazon\.com', target): + links['amazon'] = target + elif re.match(r'^https://open\.spotify\.com', target): + links['spotify'] = target + elif re.match(r'^https://.*qobuz\.com', target): + links['qobuz'] = target + elif re.match(r'^https://.*deezer\.com', target): + links['deezer'] = target + elif re.match(r'^https://.*beatport\.com', target): + links['beatport'] = target + return links def get_artist_releases(artist_id): """Fetch releases for a given artist ID from MusicBrainz.""" @@ -83,7 +133,7 @@ def get_artist_releases(artist_id): result = musicbrainzngs.browse_releases( artist=artist_id, release_type=['album'], - includes=['release-groups'] + includes=['release-groups', 'url-rels'] ) releases = [] diff --git a/mzb_rss_service/templates/feed.html b/mbz_rss_service/templates/feed.html similarity index 91% rename from mzb_rss_service/templates/feed.html rename to mbz_rss_service/templates/feed.html index 3ec97f5..01b65a9 100644 --- a/mzb_rss_service/templates/feed.html +++ b/mbz_rss_service/templates/feed.html @@ -9,6 +9,11 @@ {% for artist in feed.artists %}
  • {{ artist.name }} + {% if artist.links %} + {% for type, link in artist.links.items() %} + {{ type }}{% if not loop.last %} | {% endif %} + {% endfor %} + {% endif %}
    diff --git a/mzb_rss_service/templates/feed.xml b/mbz_rss_service/templates/feed.xml similarity index 72% rename from mzb_rss_service/templates/feed.xml rename to mbz_rss_service/templates/feed.xml index 37a6816..fd0448e 100644 --- a/mzb_rss_service/templates/feed.xml +++ b/mbz_rss_service/templates/feed.xml @@ -14,10 +14,17 @@ https://musicbrainz.org/release/{{ release.id }} {{ release.id }} + {% endif %}

    {{ release.artist.name }} {{ release.title }}
    Release Date: {{ release.date }}

    - {% if release.hasCoverArt %} - Cover Art {{ release.title }} + {% if release.links %} +

    Links: + {% for type, url in release.links.items() %} + {{ type | capitalize }}{% if not loop.last %} | {% endif %} + {% endfor %} +

    {% endif %} ]]>
    {% if release.pub_date %} From a11ec95e586c455e9ece5de218e587b53a03ac52 Mon Sep 17 00:00:00 2001 From: Volker Date: Sat, 27 Dec 2025 17:55:54 +0100 Subject: [PATCH 4/4] add service rss url (can be copied to miniflux directly) --- mbz_rss_service/config.py | 3 +++ mbz_rss_service/main.py | 4 ++++ mbz_rss_service/musicbrainz.py | 8 ++++---- mbz_rss_service/templates/index.html | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mbz_rss_service/config.py b/mbz_rss_service/config.py index c9efed6..f9a17af 100644 --- a/mbz_rss_service/config.py +++ b/mbz_rss_service/config.py @@ -14,6 +14,7 @@ CACHE_DIR = os.path.expandvars(os.environ.get('CACHE_DIR', '/var/mbz-rss-feeder/ MB_APP_NAME = os.path.expandvars(os.environ.get('MB_APP_NAME', 'mbz-rss-service')) MB_VERSION = os.path.expandvars(os.environ.get('MB_VERSION', '1')) MB_CONTACT = os.path.expandvars(os.environ.get('MB_CONTACT', 'someone@somewhere.com')) +MBZ_SERVICE_BASE_URL = os.path.expandvars(os.environ.get('MBZ_SERVICE_BASE_URL', 'http://mbz-rss-feeder:8080')) # file system dir check if not os.path.exists(CACHE_DIR): @@ -41,6 +42,8 @@ class Config: self.MB_APP_NAME = MB_APP_NAME self.MB_VERSION = MB_VERSION self.MB_CONTACT = MB_CONTACT + self.MBZ_SERVICE_BASE_URL = MBZ_SERVICE_BASE_URL + try: with open(os.path.join(os.path.dirname(__file__), '..', 'VERSION')) as f: self.VERSION = f.read().strip() diff --git a/mbz_rss_service/main.py b/mbz_rss_service/main.py index aa1e53b..78fb6fb 100644 --- a/mbz_rss_service/main.py +++ b/mbz_rss_service/main.py @@ -72,6 +72,10 @@ logger.info(f" - MusicBrainz Log Level: {mbz_log_level_str}") musicbrainz.init_musicbrainz() app = Flask(__name__) +@app.context_processor +def inject_service_name(): + return {'service_base_url': config.MBZ_SERVICE_BASE_URL} + class ReverseProxied(object): def __init__(self, app): self.app = app diff --git a/mbz_rss_service/musicbrainz.py b/mbz_rss_service/musicbrainz.py index 3dff102..fc4d0b0 100644 --- a/mbz_rss_service/musicbrainz.py +++ b/mbz_rss_service/musicbrainz.py @@ -109,7 +109,7 @@ def get_artist_relation_links(relationlist): target = rel.get('target') if not target: continue - if re.match(r'^https://.*imdb\.com', target): + if re.match(r'^https://([^/]+\.)?imdb\.com', target): links['IMDb'] = target elif re.match(r'^https://music\.apple\.com', target): links['apple'] = target @@ -117,11 +117,11 @@ def get_artist_relation_links(relationlist): links['amazon'] = target elif re.match(r'^https://open\.spotify\.com', target): links['spotify'] = target - elif re.match(r'^https://.*qobuz\.com', target): + elif re.match(r'^https://([^/]+\.)?qobuz\.com', target): links['qobuz'] = target - elif re.match(r'^https://.*deezer\.com', target): + elif re.match(r'^https://([^/]+\.)?deezer\.com', target): links['deezer'] = target - elif re.match(r'^https://.*beatport\.com', target): + elif re.match(r'^https://([^/]+\.)?beatport\.com', target): links['beatport'] = target return links diff --git a/mbz_rss_service/templates/index.html b/mbz_rss_service/templates/index.html index 839c9bc..8e33db4 100644 --- a/mbz_rss_service/templates/index.html +++ b/mbz_rss_service/templates/index.html @@ -11,7 +11,7 @@ ({{ feed.artists|length }})
    - RSS + RSS | service rss