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:
42
flake.nix
42
flake.nix
@@ -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;
|
||||||
requests
|
lidarr-mb-gap = pkgs.python3Packages.buildPythonApplication {
|
||||||
python-dotenv
|
pname = "lidarr-mb-gap";
|
||||||
]);
|
version = "1.0.0";
|
||||||
|
src = lib.cleanSource ./src;
|
||||||
|
format = "pyproject";
|
||||||
|
nativeBuildInputs = with pkgs.python3Packages; [
|
||||||
|
setuptools
|
||||||
|
];
|
||||||
|
propagatedBuildInputs = with pkgs.python3Packages; [
|
||||||
|
requests
|
||||||
|
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
2
src/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
"""Lidarr to MusicBrainz Missing Albums Finder"""
|
||||||
|
|
||||||
@@ -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")
|
||||||
@@ -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("\n✨ All 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
20
src/pyproject.toml
Normal 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"
|
||||||
|
|
||||||
Reference in New Issue
Block a user