Refactor flake.nix and enhance project structure

- Updated `flake.nix` to define a new Python application `lidarr-mb-gap` for identifying missing albums on MusicBrainz.
- Improved development shell environment by including a Python environment with necessary packages.
- Added new source files: `__init__.py`, `html_report.py`, and `main.py` to implement core functionality and HTML report generation.
- Introduced `pyproject.toml` for better package management and project metadata.
- Enhanced user instructions in the shell hook for running the application.
This commit is contained in:
Danilo Reyes
2025-11-11 10:42:34 -06:00
parent 20b07450d9
commit 7f6b998787
5 changed files with 90 additions and 39 deletions

View File

@@ -10,30 +10,46 @@
flake-utils.lib.eachDefaultSystem (system: flake-utils.lib.eachDefaultSystem (system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs { inherit system; };
pythonEnv = pkgs.python3.withPackages (ps: with ps; [ lib = pkgs.lib;
lidarr-mb-gap = pkgs.python3Packages.buildPythonApplication {
pname = "lidarr-mb-gap";
version = "1.0.0";
src = lib.cleanSource ./src;
format = "pyproject";
nativeBuildInputs = with pkgs.python3Packages; [
setuptools
];
propagatedBuildInputs = with pkgs.python3Packages; [
requests requests
python-dotenv python-dotenv
]); ];
};
in in
{ {
devShells.default = pkgs.mkShell { devShells.default = pkgs.mkShell {
buildInputs = [ buildInputs = [
pythonEnv (pkgs.python3.withPackages (ps: with ps; [
requests
python-dotenv
]))
pkgs.black pkgs.black
]; ];
shellHook = '' shellHook = ''
echo "Python environment ready!" echo "Python environment ready!"
echo "Run: python main.py" echo "Run: python src/main.py"
echo "Format code with: black main.py" echo "Format code with: black src/"
''; '';
}; };
packages.default = pkgs.writeShellApplication { packages.default = lidarr-mb-gap;
name = "lidarr-musicbrainz"; packages.lidarr-mb-gap = lidarr-mb-gap;
runtimeInputs = [ pythonEnv ]; apps.default = {
text = '' type = "app";
python ${./main.py} "$@" program = "${lidarr-mb-gap}/bin/lidarr-mb-gap";
''; };
apps.lidarr-mb-gap = {
type = "app";
program = "${lidarr-mb-gap}/bin/lidarr-mb-gap";
}; };
} }
); );

2
src/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
"""Lidarr to MusicBrainz Missing Albums Finder"""

View File

@@ -1,8 +1,11 @@
"""HTML report generation for missing albums""" """HTML report generation for missing albums"""
import logging
from html import escape from html import escape
from typing import Dict, List from typing import Dict, List
logger = logging.getLogger(__name__)
def generate_html_report(albums_to_add: List[Dict], albums_to_update: List[Dict]): def generate_html_report(albums_to_add: List[Dict], albums_to_update: List[Dict]):
"""Generate an HTML report with clickable submission links""" """Generate an HTML report with clickable submission links"""
@@ -354,4 +357,4 @@ def generate_html_report(albums_to_add: List[Dict], albums_to_update: List[Dict]
with open("missing_albums.html", "w", encoding="utf-8") as f: with open("missing_albums.html", "w", encoding="utf-8") as f:
f.write(html_content) f.write(html_content)
print(f"📄 HTML report saved to missing_albums.html") logger.info("HTML report saved to missing_albums.html")

View File

@@ -5,6 +5,7 @@ for artists monitored in Lidarr, and generate submission links.
""" """
import json import json
import logging
import os import os
import sys import sys
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
@@ -17,6 +18,13 @@ from html_report import generate_html_report
load_dotenv() load_dotenv()
logging.basicConfig(
level=logging.INFO,
format="[%(levelname)s] %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
logger = logging.getLogger(__name__)
class LidarrClient: class LidarrClient:
"""Client for interacting with Lidarr API""" """Client for interacting with Lidarr API"""
@@ -33,7 +41,7 @@ class LidarrClient:
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
print(f"Error fetching artists from Lidarr: {e}", file=sys.stderr) logger.error(f"Error fetching artists from Lidarr: {e}")
return [] return []
def get_monitored_artists( def get_monitored_artists(
@@ -254,32 +262,32 @@ def main():
MAX_ARTISTS = int(os.getenv("MAX_ARTISTS", "5")) MAX_ARTISTS = int(os.getenv("MAX_ARTISTS", "5"))
if not LIDARR_URL: if not LIDARR_URL:
print("Error: LIDARR_URL not set.", file=sys.stderr) logger.error("LIDARR_URL not set")
sys.exit(1) sys.exit(1)
if not LIDARR_API_KEY: if not LIDARR_API_KEY:
print("Error: LIDARR_API_KEY not set.", file=sys.stderr) logger.error("LIDARR_API_KEY not set")
sys.exit(1) sys.exit(1)
lidarr = LidarrClient(LIDARR_URL, LIDARR_API_KEY) lidarr = LidarrClient(LIDARR_URL, LIDARR_API_KEY)
sambl = SamblClient(SAMBL_URL) sambl = SamblClient(SAMBL_URL)
print("Fetching monitored artists from Lidarr...") logger.info("Fetching monitored artists from Lidarr...")
artists = lidarr.get_monitored_artists(["new", "all"]) artists = lidarr.get_monitored_artists(["new", "all"])
if not artists: if not artists:
print("No artists found with monitorNewItems set to 'new' or 'all'") logger.warning("No artists found with monitorNewItems set to 'new' or 'all'")
return return
total_artists = len(artists) total_artists = len(artists)
if MAX_ARTISTS > 0 and total_artists > MAX_ARTISTS: if MAX_ARTISTS > 0 and total_artists > MAX_ARTISTS:
print( logger.info(
f"Found {total_artists} monitored artists (limiting to {MAX_ARTISTS} for testing)" f"Found {total_artists} monitored artists (limiting to {MAX_ARTISTS} for testing)"
) )
artists = artists[:MAX_ARTISTS] artists = artists[:MAX_ARTISTS]
else: else:
print(f"Found {total_artists} monitored artists") logger.info(f"Found {total_artists} monitored artists")
print("\n" + "=" * 80) logger.info("=" * 80)
all_albums_to_add = [] all_albums_to_add = []
all_albums_to_update = [] all_albums_to_update = []
@@ -289,43 +297,45 @@ def main():
artist_mbid = artist.get("foreignArtistId") or artist.get("mbid") artist_mbid = artist.get("foreignArtistId") or artist.get("mbid")
if not artist_mbid: if not artist_mbid:
print(f"\n⚠️ Skipping {artist_name} - no MusicBrainz ID found") logger.warning(f"Skipping {artist_name} - no MusicBrainz ID found")
continue continue
print(f"\n🎵 Artist: {artist_name}") logger.info(f"Artist: {artist_name}")
print(f" MusicBrainz ID: {artist_mbid}") logger.info(f"MusicBrainz ID: {artist_mbid}")
albums_to_add, albums_to_update = sambl.find_missing_albums( albums_to_add, albums_to_update = sambl.find_missing_albums(
artist_mbid, artist_name artist_mbid, artist_name
) )
if albums_to_add: if albums_to_add:
print(f"\n 📥 Albums to ADD ({len(albums_to_add)}):") logger.info(f"Albums to ADD ({len(albums_to_add)}):")
processed = _process_albums(albums_to_add, "add") processed = _process_albums(albums_to_add, "add")
all_albums_to_add.extend(processed) all_albums_to_add.extend(processed)
print("\n".join(map(_format_album_output, processed))) for album_output in map(_format_album_output, processed):
logger.info(album_output)
if albums_to_update: if albums_to_update:
print(f"\n 🔄 Albums to UPDATE ({len(albums_to_update)}):") logger.info(f"Albums to UPDATE ({len(albums_to_update)}):")
processed = _process_albums(albums_to_update, "update") processed = _process_albums(albums_to_update, "update")
all_albums_to_update.extend(processed) all_albums_to_update.extend(processed)
print("\n".join(map(_format_album_output, processed))) for album_output in map(_format_album_output, processed):
logger.info(album_output)
if not albums_to_add and not albums_to_update: if not albums_to_add and not albums_to_update:
print(f"All albums are properly linked!") logger.info("All albums are properly linked!")
print("\n" + "=" * 80) logger.info("=" * 80)
print(f"\n📊 Summary:") logger.info("Summary:")
artists_info = f"Artists processed: {len(artists)}" artists_info = f"Artists processed: {len(artists)}"
if MAX_ARTISTS > 0 and total_artists > MAX_ARTISTS: if MAX_ARTISTS > 0 and total_artists > MAX_ARTISTS:
artists_info += f" (of {total_artists} total)" artists_info += f" (of {total_artists} total)"
print(artists_info) logger.info(artists_info)
print(f" Albums to ADD: {len(all_albums_to_add)}") logger.info(f"Albums to ADD: {len(all_albums_to_add)}")
print(f" Albums to UPDATE: {len(all_albums_to_update)}") logger.info(f"Albums to UPDATE: {len(all_albums_to_update)}")
all_albums = all_albums_to_add + all_albums_to_update all_albums = all_albums_to_add + all_albums_to_update
if not all_albums: if not all_albums:
print("\nAll albums are already on MusicBrainz!") logger.info("All albums are already on MusicBrainz!")
return return
output_data = { output_data = {
@@ -339,7 +349,7 @@ def main():
} }
with open("missing_albums.json", "w", encoding="utf-8") as f: with open("missing_albums.json", "w", encoding="utf-8") as f:
json.dump(output_data, f, indent=2, ensure_ascii=False) json.dump(output_data, f, indent=2, ensure_ascii=False)
print(f"\n💾 Results saved to missing_albums.json") logger.info("Results saved to missing_albums.json")
generate_html_report(all_albums_to_add, all_albums_to_update) generate_html_report(all_albums_to_add, all_albums_to_update)

20
src/pyproject.toml Normal file
View File

@@ -0,0 +1,20 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
py-modules = ["main", "html_report"]
[project]
name = "lidarr-mb-gap"
version = "1.0.0"
description = "Lidarr to MusicBrainz Missing Albums Finder"
requires-python = ">=3.8"
dependencies = [
"requests",
"python-dotenv",
]
[project.scripts]
lidarr-mb-gap = "main:main"