diff --git a/README.md b/README.md index 29b8ca7..8797d76 100644 --- a/README.md +++ b/README.md @@ -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`. 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 - # Example for the default setup: - - LIDARR_BASE=http://lidarr:8686 - ``` +| Environment Variable | Description | Source in Lidarr | +| LIDARR_BASE | The internal URL of your Lidarr instance. | N/A (Docker network alias) | +| 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 @@ -60,15 +62,19 @@ In your `docker-compose.yaml`, **only one** of these should be uncommented and s 2. In `docker-compose.yaml`, **comment out** `MBID_API_URL` and **uncomment** `MBID_JSON_FILE`: ```yaml - # ... mbid-poller environment section - environment: - # ... - - LIDARR_BASE=http://lidarr:8686 - # - MBID_API_URL= - - MBID_JSON_FILE=/config/ids.json - # - MBID_URL=... - volumes: - - ./ids.json:/config/ids.json:ro # Maps your local file into the container + # ... mbid-poller environment section + environment: + # ... + - 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_JSON_FILE=/config/ids.json + # - MBID_URL=... + volumes: + - ./ids.json:/config/ids.json:ro # Maps your local file into the container ``` ### 4. Build and Run @@ -87,4 +93,8 @@ With your files and `docker-compose.yaml` configured, you can build and run the 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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docker-compose.yaml.example b/docker-compose.yaml.example index 63fb159..9f88012 100644 --- a/docker-compose.yaml.example +++ b/docker-compose.yaml.example @@ -23,6 +23,10 @@ services: environment: # Common: The URL for your Lidarr instance - 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_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) diff --git a/poller.pl b/poller.pl index 62d4cf3..9ee2d76 100644 --- a/poller.pl +++ b/poller.pl @@ -8,16 +8,26 @@ use open qw(:std :encoding(UTF-8)); use Data::Dumper qw(Dumper); use Getopt::Std; +use HTTP::Request; use IO::Handle; -use JSON::Tiny qw(decode_json); +use JSON::Tiny qw(decode_json encode_json); use LWP::UserAgent; use Time::Duration; use URI::Escape; # --- Environment Variables --- 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 @@ -27,6 +37,17 @@ my $ua = new LWP::UserAgent; # Initialize LWP::UserAgent my $json_content; # --- 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) 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"; } $json_content = $res->content; + print STDERR "Successfully fetched " . length($json_content) . " bytes from API\n"; } # 2. Check for JSON File (Second Priority) elsif (my $json_file = $ENV{MBID_JSON_FILE}) { print STDERR "Loading IDs from JSON file: $json_file\n"; - open my $fh, '<:encoding(UTF-8)', $json_file or die "Could not open $json_file: $!"; - $json_content = do { local $/; <$fh> }; - close $fh; + if (-f $json_file) { + open my $fh, '<:encoding(UTF-8)', $json_file or die "Could not open $json_file: $!"; + $json_content = do { local $/; <$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) elsif (my $single_url = $ENV{MBID_URL}) { + print STDERR "Using single URL/ID: $single_url\n"; push @ids_to_process, $single_url; } else { die "FATAL: Must set MBID_API_URL, MBID_JSON_FILE, OR MBID_URL.\n"; } - # --- JSON Parsing Logic (Applies to API URL and JSON File) --- if ($json_content) { + print STDERR "Parsing JSON content...\n"; my $data; eval { $data = decode_json($json_content); @@ -65,9 +93,11 @@ if ($json_content) { # Extract the 'foreignId' from each object in the array if (ref $data eq 'ARRAY') { + print STDERR "Found JSON array with " . scalar(@$data) . " items\n"; foreach my $item (@$data) { if (ref $item eq 'HASH' && exists $item->{foreignId}) { push @ids_to_process, $item->{foreignId}; + print STDERR " - Added ID: $item->{foreignId}\n"; } else { warn "Skipping malformed item in input (missing 'foreignId').\n"; } @@ -77,102 +107,149 @@ 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) { chomp $id; print "\n--- Processing: $id ---\n"; + print STDERR "Processing ID: $id\n"; 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: " . 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 { - my ($type, $id) = @_; - my $ua = new LWP::UserAgent; - my $start = time(); - my $loops = 0; - my $api = "https://api.lidarr.audio/api/v0.4/$type/$id"; - my $json; + my ($type, $id) = @_; + my $ua = new LWP::UserAgent; + $ua->timeout(30); # Set reasonable timeout per request + my $start = time(); + my $loops = 0; + my $api = "https://api.lidarr.audio/api/v0.4/$type/$id"; + my $json; - print STDERR "Pinging $api\n"; - - while (1) { - $loops++; - print STDERR "- attempt $loops...\n"; - my $res = $ua->get($api); - my $status = $res->code; + print STDERR "Pinging $api\n"; - if ($res->is_success) { - eval { - $json = decode_json($res->content); - }; - if ($@) { - chomp $@; - warn "Retrying: JSON decode failed: $@\n"; - } elsif ($fresh_result and $res->header('cf-cache-status') eq 'STALE') { - $status .= ' STALE'; - warn "Retrying: Response is STALE ($status)\n"; - } else { - # *** VALIDATION CHECK *** - my @validation_warnings = validate($type, $json); - if (@validation_warnings) { - warn "Retrying: Validation failed: " . join(', ', @validation_warnings) . "\n"; + while (1) { + $loops++; + print STDERR STATUS("- attempt $loops"); + my $res = $ua->get($api); + my $status = $res->code; + + if ($res->is_success) { + eval { + $json = decode_json($res->content); + }; + if ($@) { + chomp $@; + warn "Retrying: $@\n"; + } elsif ($fresh_result and defined $res->header('cf-cache-status') and $res->header('cf-cache-status') eq 'STALE') { + $status .= ' STALE'; + print STDERR STATUS("- attempt $loops: $status (cache warming...)"); + } else { + print STDERR ERASE_EOL; + last; # Got fresh data, exit retry loop + } } else { - # Success: HTTP 2xx, JSON parsed, and content passed validation - last; + print STDERR STATUS("- attempt $loops: $status - " . $res->status_line); } - } - } else { - # Log failure if not success and retry - warn "Retrying: HTTP failed with status $status\n"; + + sleep 5; } - sleep 5; - } - my $elapsed = time() - $start; - print "- ready ($loops attempts, " . duration($elapsed) . ")\n" - if $loops > 1; - if ($type eq 'artist') { - print "- artist: " . $json->{artistname} . "\n"; - } else { - print "- album: " . $json->{title} . "\n"; - } + my $elapsed = time() - $start; + print "- ready ($loops attempts, " . duration($elapsed) . ")\n" + if $loops > 1; + if ($type eq 'artist') { + print "- artist: " . ($json->{artistname} || 'UNKNOWN') . "\n"; + } else { + print "- album: " . ($json->{title} || 'UNKNOWN') . "\n"; + } - return $json; + return $json; } sub validate { - my ($type, $json) = @_; - my @warnings; - if ($type eq 'artist') { - # make sure there are albums - unless (exists $json->{Albums} and scalar @{$json->{Albums}}) { - push(@warnings, 'no albums'); + my ($type, $json) = @_; + my @warnings; + if ($type eq 'artist') { + # make sure there are albums + unless (exists $json->{Albums} and scalar @{$json->{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; +} + +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"; + } } \ No newline at end of file