Added Lidarr API calls to add artist to library automatically. Updated README.md

This commit is contained in:
2025-09-26 12:37:04 +00:00
parent 2bcce54df7
commit 00086249b9
3 changed files with 184 additions and 93 deletions

View File

@@ -35,12 +35,14 @@ The `mbid-poller` container needs to communicate with your Lidarr instance.
1. **Rename the Example:** Rename the provided `docker-compose.yaml.example` to `docker-compose.yaml`. 1. **Rename the Example:** Rename the provided `docker-compose.yaml.example` to `docker-compose.yaml`.
2. **Network Setup:** Ensure the `lidarr` and `mbid-poller` services are on the **same Docker network** (e.g., `example_network`). If you use a different setup (like host network), update `LIDARR_BASE` accordingly. 2. **Network Setup:** Ensure the `lidarr` and `mbid-poller` services are on the **same Docker network** (e.g., `example_network`). If you use a different setup (like host network), update `LIDARR_BASE` accordingly.
3. **Lidarr Base URL:** Verify the `LIDARR_BASE` environment variable in the `mbid-poller` service points to your Lidarr instance. 3. **Lidarr Base URL:** The script uses the Lidarr API to automatically add the polled artists. You must set the following environment variables in the mbid-poller service:
```yaml | Environment Variable | Description | Source in Lidarr |
# Example for the default setup: | LIDARR_BASE | The internal URL of your Lidarr instance. | N/A (Docker network alias) |
- LIDARR_BASE=http://lidarr:8686 | LIDARR_API_KEY | Your Lidarr API key. | Settings > General > API Key |
``` | LIDARR_ROOT_FOLDER | The path for your music root folder as seen by Lidarr. | Settings > Media Management > Root Folders |
| LIDARR_QUALITY_PROFILE_ID | The numerical ID for the desired Quality Profile. (Default is 1) | Settings > Profiles > Quality Profiles (Inspect element to find ID) |
| LIDARR_METADATA_PROFILE_ID | The numerical ID for the desired Metadata Profile. (Default is 1) | Settings > Profiles > Metadata Profiles (Inspect element to find ID) |
### 3. Choose the MBID Input Source ### 3. Choose the MBID Input Source
@@ -64,6 +66,10 @@ In your `docker-compose.yaml`, **only one** of these should be uncommented and s
environment: environment:
# ... # ...
- LIDARR_BASE=http://lidarr:8686 - LIDARR_BASE=http://lidarr:8686
- LIDARR_API_KEY=your_lidarr_api_key_here
- LIDARR_ROOT_FOLDER=/music # Example
- LIDARR_QUALITY_PROFILE_ID=1 # Example
- LIDARR_METADATA_PROFILE_ID=1 # Example
# - MBID_API_URL= # - MBID_API_URL=
- MBID_JSON_FILE=/config/ids.json - MBID_JSON_FILE=/config/ids.json
# - MBID_URL=... # - MBID_URL=...
@@ -87,4 +93,8 @@ With your files and `docker-compose.yaml` configured, you can build and run the
docker compose up mbid-poller docker compose up mbid-poller
``` ```
The script will fetch the IDs, then loop through them, pinging the external MusicBrainz API until a successful response is received. It then outputs a Lidarr search URL for you to manually or automatically trigger the import in Lidarr. The script will fetch the IDs, then loop through them. For each ID, it will:
1. Ping the external MusicBrainz API until a successful response is received.
2. Use the Lidarr API to automatically add the artist and set it to monitored.
3. Output a confirmation or error message before moving to the next artist.

View File

@@ -23,6 +23,10 @@ services:
environment: environment:
# Common: The URL for your Lidarr instance # Common: The URL for your Lidarr instance
- LIDARR_BASE=http://lidarr:8686 - LIDARR_BASE=http://lidarr:8686
- LIDARR_API_KEY=your_lidarr_api_key_here # Get this from Lidarr Settings > General > API Key
- LIDARR_ROOT_FOLDER=/music # Your music root folder path in Lidarr
- LIDARR_QUALITY_PROFILE_ID=1 # ID of quality profile (check Lidarr Settings > Profiles > Quality Profiles)
- LIDARR_METADATA_PROFILE_ID=1
- MBID_API_URL= # OPTION 1: API URL (Current default, highest priority) - MBID_API_URL= # OPTION 1: API URL (Current default, highest priority)
- MBID_JSON_FILE=/config/ids.json # OPTION 2: JSON File (To use, comment out MBID_API_URL and uncomment this lines) - MBID_JSON_FILE=/config/ids.json # OPTION 2: JSON File (To use, comment out MBID_API_URL and uncomment this lines)
- MBID_URL=https://musicbrainz.org/work/69755ab1-409e-3ad7-902f-3a839042799c # OPTION 3: Single ID/URL (To use, comment out the API URL and the JSON File lines) - MBID_URL=https://musicbrainz.org/work/69755ab1-409e-3ad7-902f-3a839042799c # OPTION 3: Single ID/URL (To use, comment out the API URL and the JSON File lines)

155
poller.pl
View File

@@ -8,16 +8,26 @@ use open qw(:std :encoding(UTF-8));
use Data::Dumper qw(Dumper); use Data::Dumper qw(Dumper);
use Getopt::Std; use Getopt::Std;
use HTTP::Request;
use IO::Handle; use IO::Handle;
use JSON::Tiny qw(decode_json); use JSON::Tiny qw(decode_json encode_json);
use LWP::UserAgent; use LWP::UserAgent;
use Time::Duration; use Time::Duration;
use URI::Escape; use URI::Escape;
# --- Environment Variables --- # --- Environment Variables ---
use constant LIDARR_BASE => $ENV{LIDARR_BASE} || 'http://localhost:8686'; use constant LIDARR_BASE => $ENV{LIDARR_BASE} || 'http://localhost:8686';
use constant LIDARR_API_KEY => $ENV{LIDARR_API_KEY} || die "FATAL: LIDARR_API_KEY environment variable is required\n";
use constant LIDARR_ROOT_FOLDER => $ENV{LIDARR_ROOT_FOLDER} || '/music';
use constant LIDARR_QUALITY_PROFILE_ID => $ENV{LIDARR_QUALITY_PROFILE_ID} || 1;
use constant LIDARR_METADATA_PROFILE_ID => $ENV{LIDARR_METADATA_PROFILE_ID} || 1;
getopts('f'); # ANSI escape sequences
sub ERASE_EOL { return "\033[K"; }
sub CURSOR_BACK { return "\033[${_[0]}D"; }
sub STATUS { return $_[0] . ERASE_EOL . CURSOR_BACK(length($_[0])) }
getopts('bfhv');
my $fresh_result = defined $::opt_f ? $::opt_f : 0; # vs STALE my $fresh_result = defined $::opt_f ? $::opt_f : 0; # vs STALE
@@ -27,6 +37,17 @@ my $ua = new LWP::UserAgent; # Initialize LWP::UserAgent
my $json_content; my $json_content;
# --- Input Logic: Prioritized Checks --- # --- Input Logic: Prioritized Checks ---
print STDERR "=== MBID Poller Starting ===\n";
print STDERR "Environment variables:\n";
print STDERR " LIDARR_BASE: " . (LIDARR_BASE) . "\n";
print STDERR " LIDARR_API_KEY: " . (LIDARR_API_KEY ? "***SET***" : "NOT SET") . "\n";
print STDERR " LIDARR_ROOT_FOLDER: " . (LIDARR_ROOT_FOLDER) . "\n";
print STDERR " LIDARR_QUALITY_PROFILE_ID: " . (LIDARR_QUALITY_PROFILE_ID) . "\n";
print STDERR " LIDARR_METADATA_PROFILE_ID: " . (LIDARR_METADATA_PROFILE_ID) . "\n";
print STDERR " MBID_API_URL: " . ($ENV{MBID_API_URL} || 'NOT SET') . "\n";
print STDERR " MBID_JSON_FILE: " . ($ENV{MBID_JSON_FILE} || 'NOT SET') . "\n";
print STDERR " MBID_URL: " . ($ENV{MBID_URL} || 'NOT SET') . "\n";
print STDERR "\n";
# 1. Check for API URL (Highest Priority) # 1. Check for API URL (Highest Priority)
if (my $api_url = $ENV{MBID_API_URL}) { if (my $api_url = $ENV{MBID_API_URL}) {
@@ -36,25 +57,32 @@ if (my $api_url = $ENV{MBID_API_URL}) {
die "FATAL: Failed to fetch data from API: " . $res->status_line . "\n"; die "FATAL: Failed to fetch data from API: " . $res->status_line . "\n";
} }
$json_content = $res->content; $json_content = $res->content;
print STDERR "Successfully fetched " . length($json_content) . " bytes from API\n";
} }
# 2. Check for JSON File (Second Priority) # 2. Check for JSON File (Second Priority)
elsif (my $json_file = $ENV{MBID_JSON_FILE}) { elsif (my $json_file = $ENV{MBID_JSON_FILE}) {
print STDERR "Loading IDs from JSON file: $json_file\n"; print STDERR "Loading IDs from JSON file: $json_file\n";
if (-f $json_file) {
open my $fh, '<:encoding(UTF-8)', $json_file or die "Could not open $json_file: $!"; open my $fh, '<:encoding(UTF-8)', $json_file or die "Could not open $json_file: $!";
$json_content = do { local $/; <$fh> }; $json_content = do { local $/; <$fh> };
close $fh; close $fh;
print STDERR "Successfully loaded " . length($json_content) . " bytes from file\n";
} else {
die "FATAL: JSON file $json_file does not exist\n";
}
} }
# 3. Check for a single URL/ID (Lowest Priority) # 3. Check for a single URL/ID (Lowest Priority)
elsif (my $single_url = $ENV{MBID_URL}) { elsif (my $single_url = $ENV{MBID_URL}) {
print STDERR "Using single URL/ID: $single_url\n";
push @ids_to_process, $single_url; push @ids_to_process, $single_url;
} }
else { else {
die "FATAL: Must set MBID_API_URL, MBID_JSON_FILE, OR MBID_URL.\n"; die "FATAL: Must set MBID_API_URL, MBID_JSON_FILE, OR MBID_URL.\n";
} }
# --- JSON Parsing Logic (Applies to API URL and JSON File) --- # --- JSON Parsing Logic (Applies to API URL and JSON File) ---
if ($json_content) { if ($json_content) {
print STDERR "Parsing JSON content...\n";
my $data; my $data;
eval { eval {
$data = decode_json($json_content); $data = decode_json($json_content);
@@ -65,9 +93,11 @@ if ($json_content) {
# Extract the 'foreignId' from each object in the array # Extract the 'foreignId' from each object in the array
if (ref $data eq 'ARRAY') { if (ref $data eq 'ARRAY') {
print STDERR "Found JSON array with " . scalar(@$data) . " items\n";
foreach my $item (@$data) { foreach my $item (@$data) {
if (ref $item eq 'HASH' && exists $item->{foreignId}) { if (ref $item eq 'HASH' && exists $item->{foreignId}) {
push @ids_to_process, $item->{foreignId}; push @ids_to_process, $item->{foreignId};
print STDERR " - Added ID: $item->{foreignId}\n";
} else { } else {
warn "Skipping malformed item in input (missing 'foreignId').\n"; warn "Skipping malformed item in input (missing 'foreignId').\n";
} }
@@ -77,25 +107,48 @@ if ($json_content) {
} }
} }
print STDERR "\nTotal IDs to process: " . scalar(@ids_to_process) . "\n";
if (scalar(@ids_to_process) == 0) {
print STDERR "WARNING: No IDs found to process!\n";
exit 0;
}
# --- Main Processing Loop --- # --- Main Processing Loop (CORRECTED TO CALL ADD FUNCTION) ---
print STDERR "\n=== Starting Main Processing ===\n";
foreach my $id (@ids_to_process) { foreach my $id (@ids_to_process) {
chomp $id; chomp $id;
print "\n--- Processing: $id ---\n"; print "\n--- Processing: $id ---\n";
print STDERR "Processing ID: $id\n";
my $type = 'artist'; # Assuming 'foreignId' provides an artist MBID my $type = 'artist'; # Assuming 'foreignId' provides an artist MBID
my $json = ping_api($type, $id); # 1. Ping API to get artist data
my $artist_data = ping_api($type, $id);
# 2. Validate data (e.g., check for albums)
my @warnings = validate($type, $artist_data);
if (@warnings) {
warn "Skipping artist $id (" . ($artist_data->{artistname} || 'Unknown Artist') . ") due to warnings: " . join(', ', @warnings) . "\n";
next; # Skip to the next ID if validation fails
}
# 3. Add artist to Lidarr (This is the critical step you wanted)
add_artist_to_lidarr($artist_data);
# Print the link for reference
print "- add-to-lidarr: " . print "- add-to-lidarr: " .
LIDARR_BASE . "/add/search?term=" . uri_escape("lidarr:$id") . "\n"; LIDARR_BASE . "/add/search?term=" . uri_escape("lidarr:$id") . "\n";
} }
# --- End of Main Processing Loop ---
## subroutines print STDERR "\n=== Processing Complete ===\n";
# --- Subroutines ---
sub ping_api { sub ping_api {
my ($type, $id) = @_; my ($type, $id) = @_;
my $ua = new LWP::UserAgent; my $ua = new LWP::UserAgent;
$ua->timeout(30); # Set reasonable timeout per request
my $start = time(); my $start = time();
my $loops = 0; my $loops = 0;
my $api = "https://api.lidarr.audio/api/v0.4/$type/$id"; my $api = "https://api.lidarr.audio/api/v0.4/$type/$id";
@@ -105,7 +158,7 @@ sub ping_api {
while (1) { while (1) {
$loops++; $loops++;
print STDERR "- attempt $loops...\n"; print STDERR STATUS("- attempt $loops");
my $res = $ua->get($api); my $res = $ua->get($api);
my $status = $res->code; my $status = $res->code;
@@ -115,24 +168,18 @@ sub ping_api {
}; };
if ($@) { if ($@) {
chomp $@; chomp $@;
warn "Retrying: JSON decode failed: $@\n"; warn "Retrying: $@\n";
} elsif ($fresh_result and $res->header('cf-cache-status') eq 'STALE') { } elsif ($fresh_result and defined $res->header('cf-cache-status') and $res->header('cf-cache-status') eq 'STALE') {
$status .= ' STALE'; $status .= ' STALE';
warn "Retrying: Response is STALE ($status)\n"; print STDERR STATUS("- attempt $loops: $status (cache warming...)");
} else { } else {
# *** VALIDATION CHECK *** print STDERR ERASE_EOL;
my @validation_warnings = validate($type, $json); last; # Got fresh data, exit retry loop
if (@validation_warnings) {
warn "Retrying: Validation failed: " . join(', ', @validation_warnings) . "\n";
} else {
# Success: HTTP 2xx, JSON parsed, and content passed validation
last;
}
} }
} else { } else {
# Log failure if not success and retry print STDERR STATUS("- attempt $loops: $status - " . $res->status_line);
warn "Retrying: HTTP failed with status $status\n";
} }
sleep 5; sleep 5;
} }
@@ -140,9 +187,9 @@ sub ping_api {
print "- ready ($loops attempts, " . duration($elapsed) . ")\n" print "- ready ($loops attempts, " . duration($elapsed) . ")\n"
if $loops > 1; if $loops > 1;
if ($type eq 'artist') { if ($type eq 'artist') {
print "- artist: " . $json->{artistname} . "\n"; print "- artist: " . ($json->{artistname} || 'UNKNOWN') . "\n";
} else { } else {
print "- album: " . $json->{title} . "\n"; print "- album: " . ($json->{title} || 'UNKNOWN') . "\n";
} }
return $json; return $json;
@@ -157,22 +204,52 @@ sub validate {
push(@warnings, 'no albums'); push(@warnings, 'no albums');
} }
} }
else {
# detect Unknown Artist problem
unless (exists $json->{artists} and scalar @{$json->{artists}} and
exists $json->{artists}[0]{artistname} and
$json->{artists}[0]{artistname} !~ m%^Unknown Artist \(%) {
push(@warnings, 'no artist');
}
unless (exists $json->{Releases} and scalar @{$json->{Releases}}) {
push(@warnings, 'no releases');
}
else {
unless (exists $json->{Releases}[0]{Tracks} and
scalar @{$json->{Releases}[0]{Tracks}}) {
push(@warnings, 'no tracks');
}
}
}
return @warnings; return @warnings;
} }
sub add_artist_to_lidarr {
my ($artist_data) = @_;
print STDERR "Adding artist to Lidarr: " . $artist_data->{artistname} . "\n";
my $ua = new LWP::UserAgent;
$ua->timeout(30);
# Prepare the payload for Lidarr API
my $payload = {
'foreignArtistId' => $artist_data->{foreignArtistId},
'artistName' => $artist_data->{artistname},
'monitored' => JSON::Tiny::true,
'monitorNewItems' => 'all',
'qualityProfileId' => LIDARR_QUALITY_PROFILE_ID + 0, # Ensure it's a number
'metadataProfileId' => LIDARR_METADATA_PROFILE_ID + 0, # Ensure it's a number
'path' => LIDARR_ROOT_FOLDER . '/' . $artist_data->{artistname},
'rootFolderPath' => LIDARR_ROOT_FOLDER,
'addOptions' => {
'monitor' => 'all',
'searchForMissingAlbums' => JSON::Tiny::false
}
};
# Convert to JSON
my $json_payload = JSON::Tiny::encode_json($payload);
# Make the API request
my $lidarr_api_url = LIDARR_BASE . '/api/v1/artist';
my $req = HTTP::Request->new(POST => $lidarr_api_url);
$req->header('Content-Type' => 'application/json');
$req->header('X-Api-Key' => LIDARR_API_KEY);
$req->content($json_payload);
my $res = $ua->request($req);
if ($res->is_success) {
print "- added to Lidarr successfully\n";
} elsif ($res->code == 400) {
# Artist might already exist
print "- artist already exists in Lidarr or validation failed\n";
} else {
die "Lidarr API request failed: " . $res->status_line . " - " . $res->content . "\n";
}
}