9 Commits

Author SHA1 Message Date
droideparanoico 01e641bf41 Delete directory '.github/workflows'
Build & Deploy / build-and-deploy (push) Successful in 6s
2026-07-01 13:07:42 +02:00
droideparanoico ac1e42d614 Delete directory '.gitea/wokrflows'
Build & Deploy / build-and-deploy (push) Successful in 4s
2026-07-01 12:46:05 +02:00
droideparanoico 7d012c2e44 Add .gitea/workflows/build.yaml
Build & Deploy / build-and-deploy (push) Successful in 18s
2026-07-01 12:45:52 +02:00
droideparanoico 86c3b4d4d4 Add .gitea/wokrflows/build.yaml
Docker Publish / build-and-push-image (push) Failing after 52s
Docker Publish / tag-release (push) Has been skipped
2026-07-01 12:43:20 +02:00
Volker Kopetzky 783581ffe4 Merge pull request #9 from voko/feature/add-related-links
Feature/add related links
2025-12-27 18:25:16 +01:00
Volker a11ec95e58 add service rss url (can be copied to miniflux directly) 2025-12-27 18:15:10 +01:00
Volker 93eef59aa8 add related links
artist links to feed page (/feed/:ID)
release links to feed xml (release description)
2025-12-27 18:15:10 +01:00
Volker 6efa6617d3 fix typo: mbz is the correct string 2025-12-27 17:09:43 +01:00
Volker c97d22e0f0 version bump 1.1.2 2025-12-27 17:06:31 +01:00
17 changed files with 130 additions and 103 deletions
+31
View File
@@ -0,0 +1,31 @@
name: Build & Deploy
run-name: Build ${{ gitea.sha.substring(0, 7) }}
on:
push:
branches: [ main ]
env:
IMAGE_NAME: mbz-rss-feeder
CONTAINER_NAME: mbz-rss-feeder
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t ${{ env.IMAGE_NAME }}:${{ gitea.sha }} .
docker tag ${{ env.IMAGE_NAME }}:${{ gitea.sha }} ${{ env.IMAGE_NAME }}:latest
- name: Stop existing container (if any)
run: |
docker stop ${{ env.CONTAINER_NAME }} 2>/dev/null || true
docker rm ${{ env.CONTAINER_NAME }} 2>/dev/null || true
- name: Clean up old images
run: docker image prune -f --filter "until=24h"
-82
View File
@@ -1,82 +0,0 @@
name: Docker Publish
on:
push:
branches:
- "main"
- "fix/*"
- "feature/*"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
tag-release:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: build-and-push-image
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Read version from file
id: version
run: echo "TAG=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Create and push Git tag
run: |
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git tag -a "${{ steps.version.outputs.TAG }}" -m "Release ${{ steps.version.outputs.TAG }}"
git push origin "${{ steps.version.outputs.TAG }}"
build-and-push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Read version from file
id: version
run: echo "TAG=$(cat VERSION)" >> $GITHUB_OUTPUT
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=${{ steps.version.outputs.TAG }},enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=branch
flavor: |
latest=false
prefix=
suffix=
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build, Test and Push Docker image
uses: docker/build-push-action@v5
with:
context: .
target: final
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+3 -3
View File
@@ -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"]
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -1 +1 @@
1.1.1
1.2.0
+1
View File
@@ -0,0 +1 @@
# mbz_rss_service package
@@ -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
@@ -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/<feed_id>/artist/<artist_id>/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/<feed_id>')
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():
@@ -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 = []
@@ -9,6 +9,11 @@
{% for artist in feed.artists %}
<li>
{{ artist.name }}
{% if artist.links %}
{% for type, link in artist.links.items() %}
<a href="{{ link }}" target="_blank">{{ type }}</a>{% if not loop.last %} | {% endif %}
{% endfor %}
{% endif %}
<div class="item-actions">
<form class="form-inline" action="{{ url_for('remove_artist', feed_id=feed.id, artist_id=artist.id) }}" method="post">
<button type="submit">Remove</button>
@@ -14,10 +14,17 @@
<link>https://musicbrainz.org/release/{{ release.id }}</link>
<guid isPermaLink="false">{{ release.id }}</guid>
<description><![CDATA[
{% if release.hasCoverArt %}
<img src="https://coverartarchive.org/release/{{ release.id }}/front" alt="Cover Art {{ release.title }}">
{% endif %}
<p><b>{{ release.artist.name }}</b> <i>{{ release.title }}</i><br>
Release Date: {{ release.date }}</p>
{% if release.hasCoverArt %}
<img src="http://coverartarchive.org/release/{{ release.id }}/front-500" alt="Cover Art {{ release.title }}">
{% if release.links %}
<p><b>Links: </b>
{% for type, url in release.links.items() %}
<a href="{{ url }}" target="_blank">{{ type | capitalize }}</a>{% if not loop.last %} | {% endif %}
{% endfor %}
</p>
{% endif %}
]]></description>
{% if release.pub_date %}
@@ -11,7 +11,7 @@
({{ feed.artists|length }})
</div>
<div class="item-actions">
<a href="{{ url_for('get_feed_rss', feed_id=feed.id) }}">RSS</a>
<a href="{{ url_for('get_feed_rss', feed_id=feed.id) }}">RSS</a> | <a href="{{ service_base_url }}/feed/{{ feed.id }}">service rss</a>
<form class="form-inline" action="{{ url_for('delete_feed', feed_id=feed.id) }}" method="post">
<button type="submit">Delete</button>
</form>
-1
View File
@@ -1 +0,0 @@
# mzb_rss_service package