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/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 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 90% rename from mzb_rss_service/config.py rename to mbz_rss_service/config.py index 377ef52..f9a17af 100644 --- a/mzb_rss_service/config.py +++ b/mbz_rss_service/config.py @@ -11,9 +11,10 @@ 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')) +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() @@ -68,10 +71,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 +111,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/mzb_rss_service/main.py b/mbz_rss_service/main.py similarity index 95% rename from mzb_rss_service/main.py rename to mbz_rss_service/main.py index 3e7c3fa..78fb6fb 100644 --- a/mzb_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')) @@ -55,7 +57,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}") @@ -70,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 @@ -154,7 +160,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 +201,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 +235,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 +284,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 62% rename from mzb_rss_service/musicbrainz.py rename to mbz_rss_service/musicbrainz.py index 03e920c..fc4d0b0 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/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/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 %} diff --git a/mzb_rss_service/templates/index.html b/mbz_rss_service/templates/index.html similarity index 91% rename from mzb_rss_service/templates/index.html rename to mbz_rss_service/templates/index.html index 839c9bc..8e33db4 100644 --- a/mzb_rss_service/templates/index.html +++ b/mbz_rss_service/templates/index.html @@ -11,7 +11,7 @@ ({{ feed.artists|length }})
    - RSS + RSS | service rss 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