#!/usr/bin/env perl # -*- Perl -*- use strict; use warnings; 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 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; # 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 # Define the list of IDs to process my @ids_to_process; 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}) { print STDERR "Fetching IDs from API URL: $api_url\n"; my $res = $ua->get($api_url); unless ($res->is_success) { 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"; 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); }; if ($@) { die "Error decoding JSON from source: $@\n"; } # 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"; } } } else { die "Input content was not a JSON array.\n"; } } 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 (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 # 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 --- print STDERR "\n=== Processing Complete ===\n"; # --- Subroutines --- sub ping_api { 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 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 { print STDERR STATUS("- attempt $loops: $status - " . $res->status_line); } sleep 5; } 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; } 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'); } } 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"; } }