This commit is contained in:
Javier Casares 2026-06-03 06:25:27 +00:00
commit 650e69349f
21 changed files with 2919 additions and 268 deletions

View file

@ -218,6 +218,99 @@
font-weight: 600; font-weight: 600;
} }
/* -----------------------------------------------------------------------
Tools & Status page
----------------------------------------------------------------------- */
/* Status table */
.mra-status-table td.mra-status-dot-col {
width: 24px;
padding-right: 4px;
}
.mra-status-table th {
white-space: nowrap;
width: 180px;
font-weight: 600;
}
.mra-status-table td.mra-status-hint {
color: #50575e;
font-size: 12px;
}
/* Status indicator dots */
.mra-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
vertical-align: middle;
}
.mra-dot-ok { background: #00a32a; }
.mra-dot-warn { background: #dba617; }
.mra-dot-error { background: #d63638; }
/* Job status labels in the scheduled-jobs row */
.mra-job-label {
display: inline-block;
margin-right: 18px;
}
/* WP-CLI hint below status table */
.mra-cli-hint {
margin-top: 8px;
}
.mra-cli-hint code {
margin-right: 8px;
}
/* Operations section */
.mra-ops-heading {
margin-top: 2em;
}
.mra-destructive-heading {
margin-top: 1.5em;
color: #b32d2e;
font-size: 14px;
font-weight: 600;
}
.mra-ops-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin: 12px 0 20px;
}
.mra-op-card {
background: #fff;
border: 1px solid #dcdcde;
border-radius: 4px;
padding: 16px 20px;
width: 280px;
max-width: 100%;
display: flex;
flex-direction: column;
}
.mra-op-card-title {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: #1d2327;
}
.mra-op-card-desc {
flex: 1;
margin-bottom: 12px;
font-size: 12px;
line-height: 1.5;
}
/* Pricing reference table (settings page) */ /* Pricing reference table (settings page) */
.mra-pricing-table { .mra-pricing-table {
margin-top: 8px; margin-top: 8px;
@ -238,6 +331,44 @@
border-bottom: 1px solid #dcdcde; border-bottom: 1px solid #dcdcde;
} }
/* -----------------------------------------------------------------------
Direct scan runner
----------------------------------------------------------------------- */
.mra-runner-progress {
margin: 8px 0 10px;
}
.mra-runner-bar {
height: 8px;
background: #dcdcde;
border-radius: 4px;
overflow: hidden;
margin-bottom: 4px;
}
.mra-runner-bar-fill {
height: 100%;
width: 0;
background: #2271b1;
border-radius: 4px;
transition: width 0.25s ease;
}
.mra-runner-label {
display: block;
font-size: 12px;
color: #50575e;
min-height: 1.4em;
}
.mra-runner-actions {
display: flex;
gap: 8px;
align-items: center;
margin-top: 4px;
}
/* Consensus block */ /* Consensus block */
.mra-consensus { .mra-consensus {
background: #f0f8e8; background: #f0f8e8;

View file

@ -1,7 +1,86 @@
/* global mraAdmin */ /* global mraAdmin, mraTools */
( function ( $ ) { ( function ( $ ) {
'use strict'; 'use strict';
// -------------------------------------------------------------------------
// Batch runner — Tools page
// -------------------------------------------------------------------------
function initRunner( type ) {
var running = false;
var stopReq = false;
var $card = $( '#mra-runner-' + type );
var $start = $card.find( '.mra-runner-start' );
var $stop = $card.find( '.mra-runner-stop' );
var $prog = $card.find( '.mra-runner-progress' );
var $fill = $card.find( '.mra-runner-bar-fill' );
var $label = $card.find( '.mra-runner-label' );
$start.on( 'click', function () {
if ( running ) { return; }
running = true;
stopReq = false;
$start.prop( 'disabled', true );
$stop.prop( 'hidden', false );
$prog.prop( 'hidden', false );
$fill.css( 'width', '0%' );
$label.text( '' );
runBatch();
} );
$stop.on( 'click', function () {
stopReq = true;
$( this ).prop( 'disabled', true );
} );
function runBatch() {
if ( stopReq ) {
finish( mraTools.i18n.stopped );
return;
}
$.ajax( {
url: mraTools.ajaxUrl,
type: 'POST',
data: {
action: 'mra_run_batch',
type: type,
nonce: mraTools.nonce
},
success: function ( res ) {
if ( ! res.success ) {
finish( mraTools.i18n.error );
return;
}
var d = res.data;
var pct = d.total > 0 ? Math.round( ( d.done / d.total ) * 100 ) : 100;
$fill.css( 'width', pct + '%' );
$label.text( d.done + ' / ' + d.total );
if ( d.remaining > 0 ) {
setTimeout( runBatch, 300 );
} else {
$fill.css( 'width', '100%' );
finish( mraTools.i18n.done );
}
},
error: function () {
finish( mraTools.i18n.error );
}
} );
}
function finish( msg ) {
running = false;
$start.prop( 'disabled', false );
$stop.prop( 'hidden', true ).prop( 'disabled', false );
$label.text( msg );
}
}
// -------------------------------------------------------------------------
// Modal — Audit page
// -------------------------------------------------------------------------
var $overlay, $modalTitle, $modalBody; var $overlay, $modalTitle, $modalBody;
function openModal( attachmentId ) { function openModal( attachmentId ) {
@ -37,6 +116,11 @@
} }
$( function () { $( function () {
// Initialize batch runners if mraTools is available (Tools page only).
if ( typeof mraTools !== 'undefined' ) {
[ 'index', 'usage', 'external' ].forEach( initRunner );
}
$overlay = $( '#mra-modal-overlay' ); $overlay = $( '#mra-modal-overlay' );
$modalTitle = $overlay.find( '.mra-modal-title' ); $modalTitle = $overlay.find( '.mra-modal-title' );
$modalBody = $overlay.find( '.mra-modal-body' ); $modalBody = $overlay.find( '.mra-modal-body' );

View file

@ -1,5 +1,50 @@
== Changelog == == Changelog ==
= 1.2.0 =
_Release date: 2026-05-02_
**Added**
* Browser-based AJAX scan runner in the Tools page: run index, usage, and external scan batches directly from the browser with a live progress bar. Useful when WP-Cron is disabled or Action Scheduler is not running.
* "Requeue scan errors" operation: re-queues only attachments that failed the last external scan run (status `error`), without touching already-scanned items.
* "Last Scanned" column in the audit list, showing a human-readable relative time with a precise date tooltip. Sortable with NULLs always listed last.
* Bulk action "Export External Results CSV": downloads a CSV with attachment metadata and all external scan results, including top matching domains per provider.
**Compatibility**
* WordPress: 6.8 - 7.0
* PHP: 8.2 - 8.4
* WP-CLI: 2.x
**Tests**
* PHP Coding Standards: WPCS 3.x / PHPCS 3.x
* PHPStan: level 9
* PHPCompatibility: PHP 8.2 - 8.4
= 1.1.0 =
_Release date: 2026-05-02_
**Changed**
* Plugin URI updated to `https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit`.
* Author changed to `ROBOTSTXT` with URI `https://www.robotstxt.es/`.
* Contributors updated to `javiercasares, robotstxt`.
**Compatibility**
* WordPress: 6.8 - 7.0
* PHP: 8.2 - 8.4
* WP-CLI: 2.x
**Tests**
* PHP Coding Standards: WPCS 3.x / PHPCS 3.x
* PHPStan: level 9
* PHPCompatibility: PHP 8.2 - 8.4
= 1.0.0 = = 1.0.0 =
_Release date: 2026-05-01_ _Release date: 2026-05-01_

View file

@ -65,6 +65,8 @@ class AuditPage {
if ( 'export_csv' === $action ) { if ( 'export_csv' === $action ) {
$this->handle_export_csv(); $this->handle_export_csv();
} elseif ( 'export_external_csv' === $action ) {
$this->handle_export_external_csv();
} elseif ( 'run_external_scan' === $action ) { } elseif ( 'run_external_scan' === $action ) {
$this->handle_run_external_scan(); $this->handle_run_external_scan();
} elseif ( 'purge_external_data' === $action ) { } elseif ( 'purge_external_data' === $action ) {
@ -498,6 +500,142 @@ class AuditPage {
exit; exit;
} }
/**
* Outputs an external-results CSV for selected attachment IDs and terminates.
*
* One row per attachment × provider pair. Attachments with no results still
* appear with empty provider columns.
*
* @return void
*/
private function handle_export_external_csv(): void {
check_admin_referer( 'bulk-mra-attachments' );
if ( ! current_user_can( 'edit_posts' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-mediaaudit' ) );
}
$ids = $this->collect_attachment_ids();
$rows = $this->build_external_csv_rows( $ids );
$filename = 'media-audit-external-' . gmdate( 'Y-m-d' ) . '.csv';
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Pragma: no-cache' );
$out = fopen( 'php://output', 'w' );
if ( false === $out ) {
wp_die( esc_html__( 'Could not open output stream.', 'robotstxt-mediaaudit' ) );
}
fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
fputcsv(
$out,
array(
__( 'Attachment ID', 'robotstxt-mediaaudit' ),
__( 'Filename', 'robotstxt-mediaaudit' ),
__( 'File URL', 'robotstxt-mediaaudit' ),
__( 'External Status', 'robotstxt-mediaaudit' ),
__( 'Last Scanned', 'robotstxt-mediaaudit' ),
__( 'Provider', 'robotstxt-mediaaudit' ),
__( 'Match Count', 'robotstxt-mediaaudit' ),
__( 'Top Domains', 'robotstxt-mediaaudit' ),
)
);
foreach ( $rows as $row ) {
fputcsv( $out, $row );
}
fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
exit;
}
/**
* Builds external-results CSV rows for the given attachment IDs.
*
* Each row represents one attachment × provider combination. Attachments with
* no provider results are included with empty provider columns. If $ids is
* empty, all indexed attachments are exported.
*
* @param array<int, int> $ids Attachment IDs to export (empty = all).
*
* @return array<int, array<int, string>>
*/
private function build_external_csv_rows( array $ids ): array {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
if ( empty( $ids ) ) {
$rows = $wpdb->get_results(
"SELECT i.attachment_id, i.file_name, i.file_url, i.external_status,
i.external_scanned_at, er.provider, er.match_count, er.top_domains
FROM {$wpdb->prefix}mra_media_index i
LEFT JOIN {$wpdb->prefix}mra_external_results er ON er.attachment_id = i.attachment_id
ORDER BY i.attachment_id ASC, er.provider ASC",
ARRAY_A
);
} else {
$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT i.attachment_id, i.file_name, i.file_url, i.external_status,
i.external_scanned_at, er.provider, er.match_count, er.top_domains
FROM {$wpdb->prefix}mra_media_index i
LEFT JOIN {$wpdb->prefix}mra_external_results er ON er.attachment_id = i.attachment_id
WHERE i.attachment_id IN ({$placeholders})
ORDER BY i.attachment_id ASC, er.provider ASC",
...$ids
),
ARRAY_A
);
}
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
if ( ! $rows ) {
return array();
}
$output = array();
foreach ( $rows as $r ) {
$aid_raw = $r['attachment_id'] ?? '';
$scanned_raw = $r['external_scanned_at'] ?? '';
$domains_raw = $r['top_domains'] ?? '';
$mc_raw = $r['match_count'] ?? '';
$domains_decoded = is_string( $domains_raw ) && '' !== $domains_raw
? json_decode( $domains_raw, true )
: array();
$domain_parts = array();
if ( is_array( $domains_decoded ) ) {
foreach ( $domains_decoded as $d ) {
if ( is_string( $d ) ) {
$domain_parts[] = $d;
}
}
}
$domains_str = implode( '; ', $domain_parts );
$output[] = array(
is_numeric( $aid_raw ) ? (string) (int) $aid_raw : '',
is_string( $r['file_name'] ?? null ) ? (string) $r['file_name'] : '',
is_string( $r['file_url'] ?? null ) ? (string) $r['file_url'] : '',
is_string( $r['external_status'] ?? null ) ? (string) $r['external_status'] : '',
is_string( $scanned_raw ) ? $scanned_raw : '',
is_string( $r['provider'] ?? null ) ? (string) $r['provider'] : '',
is_numeric( $mc_raw ) ? (string) (int) $mc_raw : '',
$domains_str,
);
}
return $output;
}
/** /**
* Queues selected attachments for external scanning and redirects. * Queues selected attachments for external scanning and redirects.
* *

View file

@ -54,6 +54,7 @@ class MediaListTable extends \WP_List_Table {
'usage_count' => __( 'Usage Count', 'robotstxt-mediaaudit' ), 'usage_count' => __( 'Usage Count', 'robotstxt-mediaaudit' ),
'usages' => __( 'Usages', 'robotstxt-mediaaudit' ), 'usages' => __( 'Usages', 'robotstxt-mediaaudit' ),
'external_status' => __( 'External Status', 'robotstxt-mediaaudit' ), 'external_status' => __( 'External Status', 'robotstxt-mediaaudit' ),
'external_scanned_at' => __( 'Last Scanned', 'robotstxt-mediaaudit' ),
); );
} }
@ -66,6 +67,7 @@ class MediaListTable extends \WP_List_Table {
return array( return array(
'attachment_id' => array( 'attachment_id', true ), 'attachment_id' => array( 'attachment_id', true ),
'usage_count' => array( 'usage_count', false ), 'usage_count' => array( 'usage_count', false ),
'external_scanned_at' => array( 'external_scanned_at', false ),
); );
} }
@ -79,6 +81,7 @@ class MediaListTable extends \WP_List_Table {
'run_external_scan' => __( 'Run External Scan', 'robotstxt-mediaaudit' ), 'run_external_scan' => __( 'Run External Scan', 'robotstxt-mediaaudit' ),
'purge_external_data' => __( 'Purge External Data', 'robotstxt-mediaaudit' ), 'purge_external_data' => __( 'Purge External Data', 'robotstxt-mediaaudit' ),
'export_csv' => __( 'Export CSV', 'robotstxt-mediaaudit' ), 'export_csv' => __( 'Export CSV', 'robotstxt-mediaaudit' ),
'export_external_csv' => __( 'Export External Results CSV', 'robotstxt-mediaaudit' ),
); );
} }
@ -309,6 +312,47 @@ class MediaListTable extends \WP_List_Table {
return $out; return $out;
} }
/**
* Renders the external scan date column.
*
* Shows a human-readable time difference (e.g. "3 days ago") with the full
* date/time as a tooltip. Displays "" when the attachment has never been scanned.
*
* @param array<string, mixed> $item Row data.
*
* @return string
*/
protected function column_external_scanned_at( $item ) {
$raw = $item['external_scanned_at'];
if ( ! is_string( $raw ) || '' === $raw ) {
return '&mdash;';
}
$ts = strtotime( $raw );
if ( false === $ts ) {
return '&mdash;';
}
$date_fmt = get_option( 'date_format' );
$time_fmt = get_option( 'time_format' );
$format = ( is_string( $date_fmt ) ? $date_fmt : 'Y-m-d' )
. ' '
. ( is_string( $time_fmt ) ? $time_fmt : 'H:i' );
$full_val = wp_date( $format, $ts );
$full = is_string( $full_val ) ? $full_val : $raw;
return sprintf(
'<abbr title="%s">%s</abbr>',
esc_attr( $full ),
sprintf(
/* translators: %s: human-readable time difference, e.g. "3 days" */
esc_html__( '%s ago', 'robotstxt-mediaaudit' ),
esc_html( human_time_diff( $ts ) )
)
);
}
/** /**
* Renders filter controls above the table. * Renders filter controls above the table.
* *
@ -391,7 +435,7 @@ class MediaListTable extends \WP_List_Table {
$offset = ( $paged - 1 ) * $per_page; $offset = ( $paged - 1 ) * $per_page;
// Sorting. // Sorting.
$allowed_orderby = array( 'attachment_id', 'usage_count' ); $allowed_orderby = array( 'attachment_id', 'usage_count', 'external_scanned_at' );
// phpcs:disable WordPress.Security.NonceVerification.Recommended // phpcs:disable WordPress.Security.NonceVerification.Recommended
$orderby_raw = isset( $_REQUEST['orderby'] ) && is_string( $_REQUEST['orderby'] ) ? sanitize_key( wp_unslash( $_REQUEST['orderby'] ) ) : ''; $orderby_raw = isset( $_REQUEST['orderby'] ) && is_string( $_REQUEST['orderby'] ) ? sanitize_key( wp_unslash( $_REQUEST['orderby'] ) ) : '';
$orderby = in_array( $orderby_raw, $allowed_orderby, true ) ? $orderby_raw : 'attachment_id'; $orderby = in_array( $orderby_raw, $allowed_orderby, true ) ? $orderby_raw : 'attachment_id';
@ -438,6 +482,9 @@ class MediaListTable extends \WP_List_Table {
if ( 'usage_count' === $orderby ) { if ( 'usage_count' === $orderby ) {
$order_sql = "(SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_usage mu WHERE mu.attachment_id = i.attachment_id) {$order}"; $order_sql = "(SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_usage mu WHERE mu.attachment_id = i.attachment_id) {$order}";
} elseif ( 'external_scanned_at' === $orderby ) {
// NULLs always last, regardless of sort direction.
$order_sql = "i.external_scanned_at IS NULL ASC, i.external_scanned_at {$order}";
} else { } else {
$order_sql = "i.attachment_id {$order}"; $order_sql = "i.attachment_id {$order}";
} }

View file

@ -0,0 +1,776 @@
<?php
/**
* Tools & Status admin page.
*
* @package MediaRightsAudit\Admin
*/
namespace MediaRightsAudit\Admin;
use MediaRightsAudit\Core\Database;
use MediaRightsAudit\Core\Queue\Scheduler;
use MediaRightsAudit\External\ExternalScanner;
use MediaRightsAudit\Internal\AttachmentIndexer;
use MediaRightsAudit\Internal\UsageScanner;
/**
* Renders the Tools & Status admin page.
*
* Provides a live system-health overview and one-click bulk operations for
* managing internal and external scan data. All state-changing operations
* use POST + redirect (PRG) to prevent double-submission.
*/
class ToolsPage {
/**
* Nonce action for all tool operations.
*/
const NONCE_ACTION = 'mra_tools_op';
/**
* Nonce field name.
*/
const NONCE_FIELD = 'mra_tools_nonce';
/**
* Nonce action for the AJAX batch runner.
*/
const NONCE_BATCH = 'mra_run_batch';
/**
* Admin page hook suffix, used to scope asset enqueuing.
*
* @var string
*/
private string $hook_suffix = '';
/**
* Stores the hook suffix and registers the load-* hook for pre-output processing.
*
* @param string $suffix Hook suffix returned by add_submenu_page().
*
* @return void
*/
public function set_hook_suffix( string $suffix ): void {
$this->hook_suffix = $suffix;
add_action( 'load-' . $suffix, array( $this, 'handle_load' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
}
/**
* Enqueues CSS and JS assets for the Tools page.
*
* @param string $hook Current admin page hook suffix.
*
* @return void
*/
public function enqueue_assets( string $hook ): void {
if ( $hook !== $this->hook_suffix ) {
return;
}
$base = ROBOTSTXT_MEDIAAUDIT_PLUGIN_URL . 'assets/';
$ver = ROBOTSTXT_MEDIAAUDIT_VERSION;
wp_enqueue_style( 'mra-admin', $base . 'css/media-audit-admin.css', array(), $ver );
wp_enqueue_script( 'mra-admin', $base . 'js/media-audit-admin.js', array( 'jquery' ), $ver, true );
wp_localize_script(
'mra-admin',
'mraTools',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( self::NONCE_BATCH ),
'i18n' => array(
'done' => __( 'Done.', 'robotstxt-mediaaudit' ),
'stopped' => __( 'Stopped.', 'robotstxt-mediaaudit' ),
'error' => __( 'An error occurred.', 'robotstxt-mediaaudit' ),
),
)
);
}
/**
* AJAX handler for the batch runner. Processes one batch and returns progress data.
*
* Expected POST fields: type (index|usage|external), nonce.
* Returns JSON: {done: int, total: int, remaining: int}.
*
* @return void
*/
public function ajax_run_batch(): void {
check_ajax_referer( self::NONCE_BATCH, 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-mediaaudit' ) ), 403 );
}
$type = sanitize_key( wp_unslash( isset( $_POST['type'] ) && is_string( $_POST['type'] ) ? $_POST['type'] : '' ) );
$remaining = 0;
$done = 0;
$total = 0;
switch ( $type ) {
case 'index':
AttachmentIndexer::index_batch();
$remaining = AttachmentIndexer::get_pending_count();
$done = AttachmentIndexer::get_indexed_count();
$total = $done + $remaining;
break;
case 'usage':
UsageScanner::scan_batch();
$remaining = UsageScanner::get_pending_count();
$total = AttachmentIndexer::get_indexed_count();
$done = max( 0, $total - $remaining );
break;
case 'external':
ExternalScanner::queue_all_pending();
ExternalScanner::scan_batch();
$counts = ExternalScanner::get_status_counts();
$remaining = $counts['queued'] ?? 0;
$done = ( $counts['scanned'] ?? 0 ) + ( $counts['matches'] ?? 0 ) + ( $counts['error'] ?? 0 );
$total = $done + $remaining;
break;
default:
wp_send_json_error( array( 'message' => __( 'Invalid batch type.', 'robotstxt-mediaaudit' ) ), 400 );
}
wp_send_json_success(
array(
'done' => $done,
'total' => $total,
'remaining' => $remaining,
)
);
}
/**
* Fires before page HTML is output; processes POST operations and redirects.
*
* @return void
*/
public function handle_load(): void {
if ( ! isset( $_POST['mra_tools_op'] ) ) {
return;
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-mediaaudit' ) );
}
check_admin_referer( self::NONCE_ACTION, self::NONCE_FIELD );
$op = sanitize_key( wp_unslash( is_string( $_POST['mra_tools_op'] ) ? $_POST['mra_tools_op'] : '' ) );
$result = $this->dispatch_op( $op );
wp_safe_redirect(
add_query_arg(
array(
'page' => 'robotstxt-mediaaudit-tools',
'mra_notice' => $result['notice'],
'mra_count' => $result['count'],
'mra_op' => $op,
),
admin_url( 'admin.php' )
)
);
exit;
}
/**
* Routes an operation key to the appropriate handler and returns a result array.
*
* @param string $op Operation key from the POST body.
*
* @return array{notice: string, count: int}
*/
private function dispatch_op( string $op ): array {
$count = 0;
switch ( $op ) {
case 'schedule_internal':
AttachmentIndexer::schedule();
UsageScanner::schedule();
break;
case 'schedule_external':
ExternalScanner::schedule();
break;
case 'requeue_errors':
$count = ExternalScanner::requeue_errors();
break;
case 'rescan_usage':
$count = UsageScanner::reset_all();
UsageScanner::schedule();
break;
case 'reindex_all':
$count = AttachmentIndexer::reset_all();
AttachmentIndexer::schedule();
break;
case 'purge_external':
$count = ExternalScanner::purge_all();
break;
case 'full_reset':
Database::truncate_all();
AttachmentIndexer::schedule();
break;
default:
return array(
'notice' => 'error',
'count' => 0,
);
}
return array(
'notice' => 'success',
'count' => $count,
);
}
/**
* Renders the Tools & Status page.
*
* @return void
*/
public function render(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'robotstxt-mediaaudit' ) );
}
$status = $this->get_system_status();
$notice = $this->get_notice_data();
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php $this->render_notice( $notice ); ?>
<h2><?php esc_html_e( 'System Status', 'robotstxt-mediaaudit' ); ?></h2>
<?php $this->render_status( $status ); ?>
<h2 class="mra-ops-heading"><?php esc_html_e( 'Operations', 'robotstxt-mediaaudit' ); ?></h2>
<?php $this->render_operations(); ?>
<h2 class="mra-ops-heading"><?php esc_html_e( 'Direct scan runner', 'robotstxt-mediaaudit' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Run scan batches directly from your browser without relying on background jobs. Useful when WP-Cron is disabled or Action Scheduler is not running. Keep this tab open while processing.', 'robotstxt-mediaaudit' ); ?>
</p>
<?php $this->render_runner(); ?>
</div>
<?php
}
// -------------------------------------------------------------------------
// Status data
// -------------------------------------------------------------------------
/**
* Collects all status data for the status table.
*
* @return array{db_version_ok: bool, stored_version: string, as_available: bool, cron_disabled: bool, total_images: int, indexed: int, pending_index: int, pending_usage: int, external: array<string, int>, jobs: array{index: bool, usage: bool, external: bool}}
*/
private function get_system_status(): array {
global $wpdb;
$raw_ver = get_option( 'robotstxt_mediaaudit_db_version', '' );
$stored_ver = is_string( $raw_ver ) ? $raw_ver : '';
$as_ok = Scheduler::is_available();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$total_images = (int) $wpdb->get_var(
"SELECT COUNT(ID) FROM {$wpdb->posts}
WHERE post_type = 'attachment'
AND post_mime_type LIKE 'image/%'
AND post_status != 'trash'"
);
$indexed = AttachmentIndexer::get_indexed_count();
$pending_idx = AttachmentIndexer::get_pending_count();
$pending_use = UsageScanner::get_pending_count();
$ext_counts = ExternalScanner::get_status_counts();
$fallback_ver = '' !== $stored_ver ? $stored_ver : __( 'not installed', 'robotstxt-mediaaudit' );
return array(
'db_version_ok' => ROBOTSTXT_MEDIAAUDIT_DB_VERSION === $stored_ver,
'stored_version' => $fallback_ver,
'as_available' => $as_ok,
'cron_disabled' => defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON,
'total_images' => $total_images,
'indexed' => $indexed,
'pending_index' => $pending_idx,
'pending_usage' => $pending_use,
'external' => $ext_counts,
'jobs' => array(
'index' => $as_ok && Scheduler::has_pending( AttachmentIndexer::AS_HOOK ),
'usage' => $as_ok && Scheduler::has_pending( UsageScanner::AS_HOOK ),
'external' => $as_ok && Scheduler::has_pending( ExternalScanner::AS_HOOK ),
),
);
}
/**
* Reads PRG redirect query parameters and returns structured notice data.
*
* @return array{type: string, op: string, count: int}|null
*/
private function get_notice_data(): ?array {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['mra_notice'] ) ) {
return null;
}
$type = sanitize_key( wp_unslash( is_string( $_GET['mra_notice'] ) ? $_GET['mra_notice'] : '' ) );
$op2 = sanitize_key( wp_unslash( isset( $_GET['mra_op'] ) && is_string( $_GET['mra_op'] ) ? $_GET['mra_op'] : '' ) );
$count = isset( $_GET['mra_count'] ) && is_numeric( $_GET['mra_count'] ) ? (int) $_GET['mra_count'] : 0;
return array(
'type' => $type,
'op' => $op2,
'count' => abs( $count ),
);
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
// -------------------------------------------------------------------------
// Rendering helpers
// -------------------------------------------------------------------------
/**
* Renders a WP admin notice for a completed operation.
*
* @param array{type: string, op: string, count: int}|null $notice Notice data.
*
* @return void
*/
private function render_notice( ?array $notice ): void {
if ( null === $notice || 'success' !== $notice['type'] ) {
return;
}
$messages = array(
'schedule_internal' => __( 'Internal scan scheduled.', 'robotstxt-mediaaudit' ),
'schedule_external' => __( 'External scan scheduled.', 'robotstxt-mediaaudit' ),
'requeue_errors' => __( 'Failed scans re-queued. Background scan scheduled.', 'robotstxt-mediaaudit' ),
'rescan_usage' => __( 'Usage data cleared. Background scan scheduled.', 'robotstxt-mediaaudit' ),
'reindex_all' => __( 'Index cleared. Fresh indexing scheduled.', 'robotstxt-mediaaudit' ),
'purge_external' => __( 'External scan data purged.', 'robotstxt-mediaaudit' ),
'full_reset' => __( 'All data reset. Fresh indexing scheduled.', 'robotstxt-mediaaudit' ),
);
$msg = $messages[ $notice['op'] ] ?? __( 'Operation completed.', 'robotstxt-mediaaudit' );
?>
<div class="notice notice-success is-dismissible">
<p><?php echo esc_html( $msg ); ?></p>
</div>
<?php
}
/**
* Renders the system status table.
*
* @param array{db_version_ok: bool, stored_version: string, as_available: bool, cron_disabled: bool, total_images: int, indexed: int, pending_index: int, pending_usage: int, external: array<string, int>, jobs: array{index: bool, usage: bool, external: bool}} $status Status data.
*
* @return void
*/
private function render_status( array $status ): void {
$db_ok = $status['db_version_ok'];
$as_ok = $status['as_available'];
$cron = $status['cron_disabled'];
$tot = $status['total_images'];
$idx = $status['indexed'];
$pi = $status['pending_index'];
$pu = $status['pending_usage'];
$ext = $status['external'];
$ep = $ext['pending'] ?? 0;
$eq = $ext['queued'] ?? 0;
$es = $ext['scanned'] ?? 0;
$em = $ext['matches'] ?? 0;
$ee = $ext['error'] ?? 0;
$jobs = $status['jobs'];
$ji = $jobs['index'];
$ju = $jobs['usage'];
$je = $jobs['external'];
$needs_work = $pi > 0 || $pu > 0 || $ep > 0 || $eq > 0;
$any_job = $ji || $ju || $je;
?>
<table class="widefat striped mra-status-table">
<tbody>
<tr>
<td class="mra-status-dot-col"><?php echo $this->dot( $db_ok ? 'ok' : 'error' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<th scope="row"><?php esc_html_e( 'Database schema', 'robotstxt-mediaaudit' ); ?></th>
<td>
<?php
if ( $db_ok ) {
printf(
/* translators: %s: version number */
esc_html__( 'v%s up to date', 'robotstxt-mediaaudit' ),
esc_html( ROBOTSTXT_MEDIAAUDIT_DB_VERSION )
);
} else {
printf(
/* translators: 1: stored version, 2: expected version */
esc_html__( 'Installed: %1$s — Required: %2$s', 'robotstxt-mediaaudit' ),
esc_html( $status['stored_version'] ),
esc_html( ROBOTSTXT_MEDIAAUDIT_DB_VERSION )
);
}
?>
</td>
<td class="mra-status-hint">
<?php if ( ! $db_ok ) : ?>
<?php esc_html_e( 'Deactivate and reactivate the plugin to apply pending migrations.', 'robotstxt-mediaaudit' ); ?>
<?php endif; ?>
</td>
</tr>
<tr>
<td class="mra-status-dot-col"><?php echo $this->dot( $as_ok ? 'ok' : 'error' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<th scope="row"><?php esc_html_e( 'Action Scheduler', 'robotstxt-mediaaudit' ); ?></th>
<td>
<?php if ( $as_ok ) : ?>
<?php esc_html_e( 'Available', 'robotstxt-mediaaudit' ); ?>
<?php else : ?>
<?php esc_html_e( 'Not available', 'robotstxt-mediaaudit' ); ?>
<?php endif; ?>
</td>
<td class="mra-status-hint">
<?php if ( ! $as_ok ) : ?>
<?php esc_html_e( 'Install and activate the Action Scheduler plugin. Background scanning will not work without it.', 'robotstxt-mediaaudit' ); ?>
<?php endif; ?>
</td>
</tr>
<tr>
<td class="mra-status-dot-col"><?php echo $this->dot( $cron ? 'warn' : 'ok' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<th scope="row"><?php esc_html_e( 'WP-Cron', 'robotstxt-mediaaudit' ); ?></th>
<td>
<?php if ( $cron ) : ?>
<?php esc_html_e( 'Disabled (DISABLE_WP_CRON)', 'robotstxt-mediaaudit' ); ?>
<?php else : ?>
<?php esc_html_e( 'Enabled', 'robotstxt-mediaaudit' ); ?>
<?php endif; ?>
</td>
<td class="mra-status-hint">
<?php if ( $cron ) : ?>
<?php esc_html_e( 'Background jobs will not fire automatically. Trigger the cron externally or use WP-CLI:', 'robotstxt-mediaaudit' ); ?>
<code>wp cron event run --due-now</code>
<?php endif; ?>
</td>
</tr>
<tr>
<td class="mra-status-dot-col"><?php echo $this->dot( $pi > 0 ? 'warn' : 'ok' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<th scope="row"><?php esc_html_e( 'Internal index', 'robotstxt-mediaaudit' ); ?></th>
<td>
<?php
printf(
/* translators: 1: indexed count, 2: total images */
esc_html__( '%1$s / %2$s images indexed', 'robotstxt-mediaaudit' ),
esc_html( number_format_i18n( $idx ) ),
esc_html( number_format_i18n( $tot ) )
);
?>
</td>
<td class="mra-status-hint">
<?php if ( $pi > 0 ) : ?>
<?php
printf(
/* translators: %s: formatted count */
esc_html( _n( '%s image not yet indexed.', '%s images not yet indexed.', $pi, 'robotstxt-mediaaudit' ) ),
esc_html( number_format_i18n( $pi ) )
);
echo ' ';
esc_html_e( 'Schedule an internal scan below.', 'robotstxt-mediaaudit' );
?>
<?php endif; ?>
</td>
</tr>
<tr>
<td class="mra-status-dot-col"><?php echo $this->dot( $pu > 0 ? 'warn' : 'ok' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></td>
<th scope="row"><?php esc_html_e( 'Usage scan', 'robotstxt-mediaaudit' ); ?></th>
<td>
<?php
printf(
/* translators: 1: scanned count, 2: total indexed */
esc_html__( '%1$s / %2$s scanned for usage', 'robotstxt-mediaaudit' ),
esc_html( number_format_i18n( max( 0, $idx - $pu ) ) ),
esc_html( number_format_i18n( $idx ) )
);
?>
</td>
<td class="mra-status-hint">
<?php if ( $pu > 0 ) : ?>
<?php
printf(
/* translators: %s: formatted count */
esc_html( _n( '%s pending.', '%s pending.', $pu, 'robotstxt-mediaaudit' ) ),
esc_html( number_format_i18n( $pu ) )
);
?>
<?php endif; ?>
</td>
</tr>
<tr>
<td class="mra-status-dot-col">
<?php
if ( $ee > 0 ) {
$ext_dot = 'error';
} elseif ( $ep > 0 || $eq > 0 ) {
$ext_dot = 'warn';
} else {
$ext_dot = 'ok';
}
echo $this->dot( $ext_dot ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
</td>
<th scope="row"><?php esc_html_e( 'External scan', 'robotstxt-mediaaudit' ); ?></th>
<td>
<?php
printf(
/* translators: 1: scanned 2: matches 3: queued 4: pending 5: errors */
esc_html__( '%1$s scanned · %2$s matches · %3$s queued · %4$s pending · %5$s errors', 'robotstxt-mediaaudit' ),
esc_html( number_format_i18n( $es ) ),
esc_html( number_format_i18n( $em ) ),
esc_html( number_format_i18n( $eq ) ),
esc_html( number_format_i18n( $ep ) ),
esc_html( number_format_i18n( $ee ) )
);
?>
</td>
<td class="mra-status-hint">
<?php if ( $ee > 0 ) : ?>
<?php esc_html_e( 'Some scans failed. Check that API keys are configured correctly in Settings.', 'robotstxt-mediaaudit' ); ?>
<?php elseif ( $ep > 0 ) : ?>
<?php esc_html_e( 'Schedule an external scan below to process pending items.', 'robotstxt-mediaaudit' ); ?>
<?php endif; ?>
</td>
</tr>
<tr>
<td class="mra-status-dot-col">
<?php
if ( $as_ok && $needs_work && ! $any_job ) {
$jobs_dot = 'warn';
} elseif ( $any_job ) {
$jobs_dot = 'ok';
} else {
$jobs_dot = 'ok';
}
echo $this->dot( $jobs_dot ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
</td>
<th scope="row"><?php esc_html_e( 'Scheduled jobs', 'robotstxt-mediaaudit' ); ?></th>
<td>
<?php if ( ! $as_ok ) : ?>
<em><?php esc_html_e( 'Action Scheduler not available.', 'robotstxt-mediaaudit' ); ?></em>
<?php else : ?>
<span class="mra-job-label"><?php esc_html_e( 'Index:', 'robotstxt-mediaaudit' ); ?> <strong><?php echo $ji ? esc_html__( 'Pending', 'robotstxt-mediaaudit' ) : '&mdash;'; ?></strong></span>
<span class="mra-job-label"><?php esc_html_e( 'Usage:', 'robotstxt-mediaaudit' ); ?> <strong><?php echo $ju ? esc_html__( 'Pending', 'robotstxt-mediaaudit' ) : '&mdash;'; ?></strong></span>
<span class="mra-job-label"><?php esc_html_e( 'External:', 'robotstxt-mediaaudit' ); ?> <strong><?php echo $je ? esc_html__( 'Pending', 'robotstxt-mediaaudit' ) : '&mdash;'; ?></strong></span>
<?php endif; ?>
</td>
<td class="mra-status-hint">
<?php if ( $as_ok && $needs_work && ! $any_job ) : ?>
<?php esc_html_e( 'There is work to do but no background job is scheduled. Use the operations below.', 'robotstxt-mediaaudit' ); ?>
<?php elseif ( $cron && $any_job ) : ?>
<?php esc_html_e( 'WP-Cron is disabled. Jobs will not fire automatically.', 'robotstxt-mediaaudit' ); ?>
<?php endif; ?>
</td>
</tr>
</tbody>
</table>
<?php if ( $cron ) : ?>
<p class="description mra-cli-hint">
<?php esc_html_e( 'WP-CLI commands to process scans manually:', 'robotstxt-mediaaudit' ); ?>
<code>wp mra scan-internal</code>
<code>wp mra scan-external</code>
</p>
<?php endif; ?>
<?php
}
/**
* Renders the operations grid (normal + destructive).
*
* @return void
*/
private function render_operations(): void {
$ops = array(
array(
'op' => 'schedule_internal',
'label' => __( 'Schedule internal scan', 'robotstxt-mediaaudit' ),
'desc' => __( 'Queue a background job to index all unindexed media files and scan their usage across posts, pages, and custom fields. Safe to run at any time — will not re-process completed work.', 'robotstxt-mediaaudit' ),
'class' => 'button button-primary',
'confirm' => '',
),
array(
'op' => 'schedule_external',
'label' => __( 'Schedule external scan', 'robotstxt-mediaaudit' ),
'desc' => __( 'Mark all pending attachments as queued and launch a background reverse-image-search via the configured providers. Requires API keys in Settings.', 'robotstxt-mediaaudit' ),
'class' => 'button button-primary',
'confirm' => '',
),
array(
'op' => 'requeue_errors',
'label' => __( 'Requeue scan errors', 'robotstxt-mediaaudit' ),
'desc' => __( 'Re-queues only the attachments that failed with an error on the last external scan run. Does not affect already-scanned items. Useful to retry after fixing API key issues.', 'robotstxt-mediaaudit' ),
'class' => 'button',
'confirm' => '',
),
);
$destructive = array(
array(
'op' => 'rescan_usage',
'label' => __( 'Reset usage data', 'robotstxt-mediaaudit' ),
'desc' => __( 'Clears all usage rows and resets the usage-scan flag for every indexed attachment. File metadata in the index is preserved. A background usage scan is scheduled immediately after.', 'robotstxt-mediaaudit' ),
'class' => 'button',
'confirm' => __( 'This will delete all usage data. Continue?', 'robotstxt-mediaaudit' ),
),
array(
'op' => 'reindex_all',
'label' => __( 'Re-index media library', 'robotstxt-mediaaudit' ),
'desc' => __( 'Deletes all index and usage entries, then schedules a full fresh re-index. External scan results are not affected. Useful if many media files have been added or removed.', 'robotstxt-mediaaudit' ),
'class' => 'button',
'confirm' => __( 'This will delete all index and usage data. Continue?', 'robotstxt-mediaaudit' ),
),
array(
'op' => 'purge_external',
'label' => __( 'Purge external data', 'robotstxt-mediaaudit' ),
'desc' => __( 'Deletes all external scan results and resets every attachment\'s external status to pending. Use this before switching API providers or to force a complete re-scan.', 'robotstxt-mediaaudit' ),
'class' => 'button',
'confirm' => __( 'This will delete all external scan results. Continue?', 'robotstxt-mediaaudit' ),
),
array(
'op' => 'full_reset',
'label' => __( 'Reset all data', 'robotstxt-mediaaudit' ),
'desc' => __( 'Truncates all three plugin tables (index, usage, external results) and schedules a fresh indexing run. Use this to start completely from scratch.', 'robotstxt-mediaaudit' ),
'class' => 'button button-link-delete',
'confirm' => __( 'This will permanently delete ALL plugin data and cannot be undone. Are you absolutely sure?', 'robotstxt-mediaaudit' ),
),
);
?>
<div class="mra-ops-grid">
<?php foreach ( $ops as $op ) : ?>
<?php $this->render_op_card( $op ); ?>
<?php endforeach; ?>
</div>
<h3 class="mra-destructive-heading">
<?php esc_html_e( 'Destructive operations', 'robotstxt-mediaaudit' ); ?>
</h3>
<p class="description">
<?php esc_html_e( 'These operations permanently delete plugin data. Intended for forced rescans, troubleshooting, or starting fresh. A confirmation prompt will appear before any action is taken.', 'robotstxt-mediaaudit' ); ?>
</p>
<div class="mra-ops-grid">
<?php foreach ( $destructive as $op ) : ?>
<?php $this->render_op_card( $op ); ?>
<?php endforeach; ?>
</div>
<?php
}
/**
* Renders a single operation card with a self-contained POST form.
*
* @param array{op: string, label: string, desc: string, class: string, confirm: string} $op Operation definition.
*
* @return void
*/
private function render_op_card( array $op ): void {
?>
<div class="mra-op-card">
<h4 class="mra-op-card-title"><?php echo esc_html( $op['label'] ); ?></h4>
<p class="mra-op-card-desc description"><?php echo esc_html( $op['desc'] ); ?></p>
<form method="post">
<?php wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD ); ?>
<input type="hidden" name="mra_tools_op" value="<?php echo esc_attr( $op['op'] ); ?>" />
<button
type="submit"
class="<?php echo esc_attr( $op['class'] ); ?>"
<?php if ( ! empty( $op['confirm'] ) ) : ?>
onclick="return confirm('<?php echo esc_js( $op['confirm'] ); ?>');"
<?php endif; ?>
>
<?php echo esc_html( $op['label'] ); ?>
</button>
</form>
</div>
<?php
}
/**
* Renders the direct scan runner cards.
*
* @return void
*/
private function render_runner(): void {
$runners = array(
array(
'type' => 'index',
'label' => __( 'Index media files', 'robotstxt-mediaaudit' ),
'desc' => __( 'Discovers unindexed images and registers them in the plugin index. Run this first.', 'robotstxt-mediaaudit' ),
),
array(
'type' => 'usage',
'label' => __( 'Scan usage', 'robotstxt-mediaaudit' ),
'desc' => __( 'Checks every indexed image for usage across posts, pages, and custom fields.', 'robotstxt-mediaaudit' ),
),
array(
'type' => 'external',
'label' => __( 'External scan', 'robotstxt-mediaaudit' ),
'desc' => __( 'Sends images to external providers for reverse-image search. Requires API keys in Settings.', 'robotstxt-mediaaudit' ),
),
);
?>
<div class="mra-ops-grid">
<?php foreach ( $runners as $runner ) : ?>
<div class="mra-op-card" id="mra-runner-<?php echo esc_attr( $runner['type'] ); ?>">
<h4 class="mra-op-card-title"><?php echo esc_html( $runner['label'] ); ?></h4>
<p class="mra-op-card-desc description"><?php echo esc_html( $runner['desc'] ); ?></p>
<div class="mra-runner-progress" hidden>
<div class="mra-runner-bar">
<div class="mra-runner-bar-fill"></div>
</div>
<span class="mra-runner-label"></span>
</div>
<div class="mra-runner-actions">
<button type="button" class="button button-primary mra-runner-start">
<?php esc_html_e( 'Run now', 'robotstxt-mediaaudit' ); ?>
</button>
<button type="button" class="button mra-runner-stop" hidden>
<?php esc_html_e( 'Stop', 'robotstxt-mediaaudit' ); ?>
</button>
</div>
</div>
<?php endforeach; ?>
</div>
<?php
}
/**
* Returns an escaped HTML status indicator dot span.
*
* @param string $color One of 'ok', 'warn', 'error'.
*
* @return string
*/
private function dot( string $color ): string {
return '<span class="mra-dot mra-dot-' . esc_attr( $color ) . '" aria-hidden="true"></span>';
}
}

View file

@ -57,6 +57,23 @@ class Database {
$wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}mra_media_index`" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->query( "DROP TABLE IF EXISTS `{$wpdb->prefix}mra_media_index`" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
} }
/**
* Truncates all plugin tables, removing all data while preserving the table structure.
*
* Used by the Tools page "Reset all data" operation.
*
* @return void
*/
public static function truncate_all(): void {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_external_results`" );
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_media_usage`" );
$wpdb->query( "TRUNCATE TABLE `{$wpdb->prefix}mra_media_index`" );
// phpcs:enable WordPress.DB.DirectDatabaseQuery
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Migrations // Migrations
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

View file

@ -9,6 +9,7 @@ namespace MediaRightsAudit\Core;
use MediaRightsAudit\Admin\AuditPage; use MediaRightsAudit\Admin\AuditPage;
use MediaRightsAudit\Admin\Settings; use MediaRightsAudit\Admin\Settings;
use MediaRightsAudit\Admin\ToolsPage;
use MediaRightsAudit\CLI\Command; use MediaRightsAudit\CLI\Command;
use MediaRightsAudit\External\AbstractProvider; use MediaRightsAudit\External\AbstractProvider;
use MediaRightsAudit\External\ExternalScanner; use MediaRightsAudit\External\ExternalScanner;
@ -38,12 +39,20 @@ class Plugin {
*/ */
private AuditPage $audit_page; private AuditPage $audit_page;
/**
* Tools & Status page controller.
*
* @var ToolsPage
*/
private ToolsPage $tools_page;
/** /**
* Initialises dependencies. * Initialises dependencies.
*/ */
public function __construct() { public function __construct() {
$this->settings = new Settings(); $this->settings = new Settings();
$this->audit_page = new AuditPage(); $this->audit_page = new AuditPage();
$this->tools_page = new ToolsPage();
} }
/** /**
@ -59,6 +68,7 @@ class Plugin {
add_action( AttachmentIndexer::AS_HOOK, array( AttachmentIndexer::class, 'process_scheduled_batch' ) ); add_action( AttachmentIndexer::AS_HOOK, array( AttachmentIndexer::class, 'process_scheduled_batch' ) );
add_action( UsageScanner::AS_HOOK, array( UsageScanner::class, 'process_scheduled_batch' ) ); add_action( UsageScanner::AS_HOOK, array( UsageScanner::class, 'process_scheduled_batch' ) );
add_action( ExternalScanner::AS_HOOK, array( ExternalScanner::class, 'process_scheduled_batch' ) ); add_action( ExternalScanner::AS_HOOK, array( ExternalScanner::class, 'process_scheduled_batch' ) );
add_action( 'wp_ajax_mra_run_batch', array( $this->tools_page, 'ajax_run_batch' ) );
add_filter( 'mra/external/providers', array( $this, 'register_providers' ) ); add_filter( 'mra/external/providers', array( $this, 'register_providers' ) );
add_filter( 'wp_privacy_personal_data_exporters', array( DataExporter::class, 'register' ) ); add_filter( 'wp_privacy_personal_data_exporters', array( DataExporter::class, 'register' ) );
add_filter( 'wp_privacy_personal_data_erasers', array( DataEraser::class, 'register' ) ); add_filter( 'wp_privacy_personal_data_erasers', array( DataEraser::class, 'register' ) );
@ -107,6 +117,19 @@ class Plugin {
array( $this->audit_page, 'render' ) array( $this->audit_page, 'render' )
); );
$tools_suffix = add_submenu_page(
'robotstxt-mediaaudit',
__( 'Tools', 'robotstxt-mediaaudit' ),
__( 'Tools', 'robotstxt-mediaaudit' ),
'manage_options',
'robotstxt-mediaaudit-tools',
array( $this->tools_page, 'render' )
);
if ( is_string( $tools_suffix ) ) {
$this->tools_page->set_hook_suffix( $tools_suffix );
}
add_submenu_page( add_submenu_page(
'robotstxt-mediaaudit', 'robotstxt-mediaaudit',
__( 'Settings', 'robotstxt-mediaaudit' ), __( 'Settings', 'robotstxt-mediaaudit' ),

View file

@ -184,6 +184,39 @@ class ExternalScanner {
} }
} }
/**
* Re-queues all attachments that previously failed with external_status = 'error'.
*
* Does not touch pending, queued, scanned, or matched attachments.
* Schedules a background scan batch if any items were re-queued and
* no batch is already pending.
*
* @return int Number of attachments re-queued.
*/
public static function requeue_errors(): int {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery
$count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_index WHERE external_status = 'error'"
);
if ( $count > 0 ) {
$wpdb->query(
"UPDATE {$wpdb->prefix}mra_media_index
SET external_status = 'queued', external_scanned_at = NULL
WHERE external_status = 'error'"
);
if ( ! Scheduler::has_pending( self::AS_HOOK ) ) {
Scheduler::schedule_single( self::AS_HOOK );
}
}
// phpcs:enable WordPress.DB.DirectDatabaseQuery
return $count;
}
/** /**
* Deletes external scan results and resets status to pending for given IDs. * Deletes external scan results and resets status to pending for given IDs.
* *
@ -221,6 +254,62 @@ class ExternalScanner {
// phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare // phpcs:enable WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
} }
/**
* Deletes all external scan results and resets every attachment's external_status to pending.
*
* @return int Number of result rows deleted.
*/
public static function purge_all(): int {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}mra_external_results" );
$wpdb->query( "DELETE FROM {$wpdb->prefix}mra_external_results" );
$wpdb->query(
"UPDATE {$wpdb->prefix}mra_media_index
SET external_status = 'pending', external_scanned_at = NULL"
);
// phpcs:enable WordPress.DB.DirectDatabaseQuery
return $count;
}
/**
* Returns a count of attachments grouped by external_status.
*
* @return array<string, int>
*/
public static function get_status_counts(): array {
global $wpdb;
$rows = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
"SELECT external_status, COUNT(*) AS cnt
FROM {$wpdb->prefix}mra_media_index
GROUP BY external_status",
ARRAY_A
);
$counts = array(
'pending' => 0,
'queued' => 0,
'scanned' => 0,
'matches' => 0,
'error' => 0,
);
if ( is_array( $rows ) ) {
foreach ( $rows as $row ) {
$s = is_array( $row ) && is_string( $row['external_status'] ) ? $row['external_status'] : '';
if ( isset( $counts[ $s ] ) ) {
$cnt = isset( $row['cnt'] ) && is_numeric( $row['cnt'] ) ? (int) $row['cnt'] : 0;
$counts[ $s ] = $cnt;
}
}
}
return $counts;
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Private helpers // Private helpers
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------

View file

@ -172,4 +172,21 @@ class AttachmentIndexer {
Scheduler::schedule_single( self::AS_HOOK ); Scheduler::schedule_single( self::AS_HOOK );
} }
} }
/**
* Deletes all rows from mra_media_index and mra_media_usage, forcing a full re-index on the next run.
*
* @return int Number of index rows removed.
*/
public static function reset_all(): int {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery
$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_index" );
$wpdb->query( "DELETE FROM {$wpdb->prefix}mra_media_usage" );
$wpdb->query( "DELETE FROM {$wpdb->prefix}mra_media_index" );
// phpcs:enable WordPress.DB.DirectDatabaseQuery
return $count;
}
} }

View file

@ -461,4 +461,26 @@ class UsageScanner {
Scheduler::schedule_single( self::AS_HOOK ); Scheduler::schedule_single( self::AS_HOOK );
} }
} }
/**
* Resets usage scan state for all indexed attachments.
*
* Clears internal_scanned_at so every attachment is re-queued for scanning, and
* deletes all mra_media_usage rows. File metadata in mra_media_index is preserved.
*
* @return int Number of attachments reset.
*/
public static function reset_all(): int {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery
$count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->prefix}mra_media_index WHERE internal_scanned_at IS NOT NULL"
);
$wpdb->query( "UPDATE {$wpdb->prefix}mra_media_index SET internal_scanned_at = NULL" );
$wpdb->query( "DELETE FROM {$wpdb->prefix}mra_media_usage" );
// phpcs:enable WordPress.DB.DirectDatabaseQuery
return $count;
}
} }

Binary file not shown.

View file

@ -2,10 +2,10 @@
# This file is distributed under the GPL-3.0-or-later. # This file is distributed under the GPL-3.0-or-later.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Media Audit (by ROBOTSTXT) 1.0.0\n" "Project-Id-Version: Media Audit (by ROBOTSTXT) 1.2.0\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-"
"mediaaudit\n" "mediaaudit\n"
"POT-Creation-Date: 2026-05-02T06:06:40+00:00\n" "POT-Creation-Date: 2026-05-02T07:36:54+00:00\n"
"PO-Revision-Date: 2026-05-01 07:41+0000\n" "PO-Revision-Date: 2026-05-01 07:41+0000\n"
"Last-Translator: Javier Casares <javier@casares.org>\n" "Last-Translator: Javier Casares <javier@casares.org>\n"
"Language-Team: Catalan <ca@li.org>\n" "Language-Team: Catalan <ca@li.org>\n"
@ -23,8 +23,8 @@ msgstr "Auditoria de mitjans (by ROBOTSTXT)"
#. Plugin URI of the plugin #. Plugin URI of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "https://robotstxt.es/plugins/media-audit/" msgid "https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit"
msgstr "https://robotstxt.es/plugins/media-audit/" msgstr ""
#. Description of the plugin #. Description of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
@ -37,279 +37,311 @@ msgstr ""
#. Author of the plugin #. Author of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "Javier Casares" msgid "ROBOTSTXT"
msgstr "Javier Casares" msgstr ""
#. Author URI of the plugin #. Author URI of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "https://javiercasares.com" msgid "https://www.robotstxt.es/"
msgstr "https://javiercasares.com" msgstr ""
#: includes/Admin/AuditPage.php:112 #: includes/Admin/AuditPage.php:114
msgid "Loading…" msgid "Loading…"
msgstr "S'està carregant…" msgstr "S'està carregant…"
#: includes/Admin/AuditPage.php:113 #: includes/Admin/AuditPage.php:115
msgid "Error loading details." msgid "Error loading details."
msgstr "S'ha produït un error en carregar els detalls." msgstr "S'ha produït un error en carregar els detalls."
#: includes/Admin/AuditPage.php:114 includes/Admin/AuditPage.php:190 #: includes/Admin/AuditPage.php:116 includes/Admin/AuditPage.php:192
msgid "This image is not used in any post." msgid "This image is not used in any post."
msgstr "Aquesta imatge no s'utilitza en cap entrada." msgstr "Aquesta imatge no s'utilitza en cap entrada."
#: includes/Admin/AuditPage.php:115 includes/Admin/AuditPage.php:152 #: includes/Admin/AuditPage.php:117 includes/Admin/AuditPage.php:154
msgid "Usage Details" msgid "Usage Details"
msgstr "Detalls d'ús" msgstr "Detalls d'ús"
#: includes/Admin/AuditPage.php:131 includes/Admin/Settings.php:325 #: includes/Admin/AuditPage.php:133 includes/Admin/Settings.php:325
#: includes/Admin/ToolsPage.php:245
msgid "You do not have permission to access this page." msgid "You do not have permission to access this page."
msgstr "No teniu permís per accedir a aquesta pàgina." msgstr "No teniu permís per accedir a aquesta pàgina."
#: includes/Admin/AuditPage.php:138 includes/Core/Plugin.php:89 #: includes/Admin/AuditPage.php:140 includes/Core/Plugin.php:99
#: includes/Core/Plugin.php:90 includes/Core/Plugin.php:103 #: includes/Core/Plugin.php:100 includes/Core/Plugin.php:113
#: includes/Core/Plugin.php:104 includes/Privacy/DataEraser.php:33 #: includes/Core/Plugin.php:114 includes/Privacy/DataEraser.php:33
#: includes/Privacy/DataExporter.php:45 #: includes/Privacy/DataExporter.php:45
msgid "Media Audit" msgid "Media Audit"
msgstr "Auditoria de mitjans" msgstr "Auditoria de mitjans"
#: includes/Admin/AuditPage.php:144 #: includes/Admin/AuditPage.php:146
msgid "Search" msgid "Search"
msgstr "Cerca" msgstr "Cerca"
#: includes/Admin/AuditPage.php:153 #: includes/Admin/AuditPage.php:155
msgid "Close" msgid "Close"
msgstr "Tanca" msgstr "Tanca"
#: includes/Admin/AuditPage.php:171 includes/Admin/AuditPage.php:459 #: includes/Admin/AuditPage.php:173 includes/Admin/AuditPage.php:461
#: includes/Admin/AuditPage.php:510 includes/Admin/AuditPage.php:542 #: includes/Admin/AuditPage.php:515 includes/Admin/AuditPage.php:648
#: includes/Admin/AuditPage.php:680 includes/Admin/ToolsPage.php:105
#: includes/Admin/ToolsPage.php:161
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Permisos insuficients." msgstr "Permisos insuficients."
#: includes/Admin/AuditPage.php:176 #: includes/Admin/AuditPage.php:178
msgid "Invalid attachment ID." msgid "Invalid attachment ID."
msgstr "Identificador de fitxer adjunt no vàlid." msgstr "Identificador de fitxer adjunt no vàlid."
#: includes/Admin/AuditPage.php:184 #: includes/Admin/AuditPage.php:186
msgid "Featured Image" msgid "Featured Image"
msgstr "Imatge destacada" msgstr "Imatge destacada"
#: includes/Admin/AuditPage.php:185 #: includes/Admin/AuditPage.php:187
msgid "Post Content" msgid "Post Content"
msgstr "Contingut de l'entrada" msgstr "Contingut de l'entrada"
#: includes/Admin/AuditPage.php:186 #: includes/Admin/AuditPage.php:188
msgid "Custom Field" msgid "Custom Field"
msgstr "Camp personalitzat" msgstr "Camp personalitzat"
#: includes/Admin/AuditPage.php:194 #: includes/Admin/AuditPage.php:196
msgid "Post" msgid "Post"
msgstr "Entrada" msgstr "Entrada"
#: includes/Admin/AuditPage.php:195 #: includes/Admin/AuditPage.php:197
msgid "Type" msgid "Type"
msgstr "Tipus" msgstr "Tipus"
#: includes/Admin/AuditPage.php:196 #: includes/Admin/AuditPage.php:198
msgid "Context" msgid "Context"
msgstr "Context" msgstr "Context"
#: includes/Admin/AuditPage.php:197 #: includes/Admin/AuditPage.php:199
msgid "Status" msgid "Status"
msgstr "Estat" msgstr "Estat"
#: includes/Admin/AuditPage.php:240 #: includes/Admin/AuditPage.php:242
msgid "Internal Usage" msgid "Internal Usage"
msgstr "Ús intern" msgstr "Ús intern"
#: includes/Admin/AuditPage.php:288 #: includes/Admin/AuditPage.php:290
msgid "External Scan Results" msgid "External Scan Results"
msgstr "Resultats de l'exploració externa" msgstr "Resultats de l'exploració externa"
#. translators: %d: number of external matches #. translators: %d: number of external matches
#: includes/Admin/AuditPage.php:312 #: includes/Admin/AuditPage.php:314
#, php-format #, php-format
msgid "%d match" msgid "%d match"
msgid_plural "%d matches" msgid_plural "%d matches"
msgstr[0] "%d coincidència" msgstr[0] "%d coincidència"
msgstr[1] "%d coincidències" msgstr[1] "%d coincidències"
#: includes/Admin/AuditPage.php:317 #: includes/Admin/AuditPage.php:319
msgid "No matches" msgid "No matches"
msgstr "Sense coincidències" msgstr "Sense coincidències"
#: includes/Admin/AuditPage.php:328 #: includes/Admin/AuditPage.php:330
msgid "Domain" msgid "Domain"
msgstr "Domini" msgstr "Domini"
#: includes/Admin/AuditPage.php:329 #: includes/Admin/AuditPage.php:331
msgid "Occurrences" msgid "Occurrences"
msgstr "Aparicions" msgstr "Aparicions"
#: includes/Admin/AuditPage.php:350 #: includes/Admin/AuditPage.php:352
msgid "Consensus Domains" msgid "Consensus Domains"
msgstr "Dominis de consens" msgstr "Dominis de consens"
#. translators: %d: number of providers that agree #. translators: %d: number of providers that agree
#: includes/Admin/AuditPage.php:356 #: includes/Admin/AuditPage.php:358
#, php-format #, php-format
msgid "%d provider" msgid "%d provider"
msgid_plural "%d providers" msgid_plural "%d providers"
msgstr[0] "%d proveïdor" msgstr[0] "%d proveïdor"
msgstr[1] "%d proveïdors" msgstr[1] "%d proveïdors"
#: includes/Admin/AuditPage.php:388 #: includes/Admin/AuditPage.php:390
msgid "In Index" msgid "In Index"
msgstr "A l'índex" msgstr "A l'índex"
#. translators: %d: count of images not yet indexed #. translators: %d: count of images not yet indexed
#: includes/Admin/AuditPage.php:392 #: includes/Admin/AuditPage.php:394
#, php-format #, php-format
msgid "%d not yet indexed" msgid "%d not yet indexed"
msgstr "%d encara no indexades" msgstr "%d encara no indexades"
#: includes/Admin/AuditPage.php:395 #: includes/Admin/AuditPage.php:397
msgid "All indexed" msgid "All indexed"
msgstr "Totes indexades" msgstr "Totes indexades"
#: includes/Admin/AuditPage.php:400 includes/Admin/MediaListTable.php:275 #: includes/Admin/AuditPage.php:402 includes/Admin/MediaListTable.php:278
msgid "Scanned" msgid "Scanned"
msgstr "Explorades" msgstr "Explorades"
#. translators: %d: percentage #. translators: %d: percentage
#: includes/Admin/AuditPage.php:403 #: includes/Admin/AuditPage.php:405
#, php-format #, php-format
msgid "%d%% of index" msgid "%d%% of index"
msgstr "%d%% de l'índex" msgstr "%d%% de l'índex"
#: includes/Admin/AuditPage.php:410 #: includes/Admin/AuditPage.php:412
msgid "Used" msgid "Used"
msgstr "Usades" msgstr "Usades"
#: includes/Admin/AuditPage.php:416 includes/Admin/MediaListTable.php:200 #: includes/Admin/AuditPage.php:418 includes/Admin/MediaListTable.php:203
msgid "Unused" msgid "Unused"
msgstr "No usades" msgstr "No usades"
#: includes/Admin/AuditPage.php:423 #: includes/Admin/AuditPage.php:425
msgid "External Matches" msgid "External Matches"
msgstr "Coincidències externes" msgstr "Coincidències externes"
#: includes/Admin/AuditPage.php:473 #: includes/Admin/AuditPage.php:475 includes/Admin/AuditPage.php:529
msgid "Could not open output stream." msgid "Could not open output stream."
msgstr "No s'ha pogut obrir el flux de sortida." msgstr "No s'ha pogut obrir el flux de sortida."
#: includes/Admin/AuditPage.php:482 includes/Privacy/DataExporter.php:257 #: includes/Admin/AuditPage.php:484 includes/Admin/AuditPage.php:537
#: includes/Privacy/DataExporter.php:299 #: includes/Privacy/DataExporter.php:257 includes/Privacy/DataExporter.php:299
msgid "Attachment ID" msgid "Attachment ID"
msgstr "ID del fitxer adjunt" msgstr "ID del fitxer adjunt"
#: includes/Admin/AuditPage.php:483 includes/Admin/MediaListTable.php:52 #: includes/Admin/AuditPage.php:485 includes/Admin/AuditPage.php:538
#: includes/Privacy/DataExporter.php:261 #: includes/Admin/MediaListTable.php:52 includes/Privacy/DataExporter.php:261
msgid "Filename" msgid "Filename"
msgstr "Nom de fitxer" msgstr "Nom de fitxer"
#: includes/Admin/AuditPage.php:484 includes/Privacy/DataExporter.php:265 #: includes/Admin/AuditPage.php:486 includes/Admin/AuditPage.php:539
#: includes/Privacy/DataExporter.php:265
msgid "File URL" msgid "File URL"
msgstr "URL del fitxer" msgstr "URL del fitxer"
#: includes/Admin/AuditPage.php:485 #: includes/Admin/AuditPage.php:487
msgid "MIME Type" msgid "MIME Type"
msgstr "Tipus MIME" msgstr "Tipus MIME"
#: includes/Admin/AuditPage.php:486 #: includes/Admin/AuditPage.php:488
msgid "File Size (bytes)" msgid "File Size (bytes)"
msgstr "Mida del fitxer (bytes)" msgstr "Mida del fitxer (bytes)"
#: includes/Admin/AuditPage.php:487 includes/Admin/MediaListTable.php:56 #: includes/Admin/AuditPage.php:489 includes/Admin/AuditPage.php:540
#: includes/Admin/MediaListTable.php:56
msgid "External Status" msgid "External Status"
msgstr "Estat extern" msgstr "Estat extern"
#: includes/Admin/AuditPage.php:488 includes/Admin/MediaListTable.php:54 #: includes/Admin/AuditPage.php:490 includes/Admin/MediaListTable.php:54
msgid "Usage Count" msgid "Usage Count"
msgstr "Nombre d'usos" msgstr "Nombre d'usos"
#: includes/Admin/AuditPage.php:489 #: includes/Admin/AuditPage.php:491
msgid "Used In (post titles)" msgid "Used In (post titles)"
msgstr "Usat a (títols d'entrades)" msgstr "Usat a (títols d'entrades)"
#: includes/Admin/AuditPage.php:541 includes/Admin/MediaListTable.php:57
msgid "Last Scanned"
msgstr "Darrera exploració"
#: includes/Admin/AuditPage.php:542 includes/Privacy/DataExporter.php:303
msgid "Provider"
msgstr "Proveïdor"
#: includes/Admin/AuditPage.php:543 includes/Privacy/DataExporter.php:307
msgid "Match Count"
msgstr "Nombre de coincidències"
#: includes/Admin/AuditPage.php:544
msgid "Top Domains"
msgstr "Dominis principals"
#: includes/Admin/MediaListTable.php:51 #: includes/Admin/MediaListTable.php:51
msgid "Thumbnail" msgid "Thumbnail"
msgstr "Miniatura" msgstr "Miniatura"
#: includes/Admin/MediaListTable.php:53 #: includes/Admin/MediaListTable.php:53
msgid "ID" msgid "ID"
msgstr "" msgstr "ID"
#: includes/Admin/MediaListTable.php:55 #: includes/Admin/MediaListTable.php:55
msgid "Usages" msgid "Usages"
msgstr "Usos" msgstr "Usos"
#: includes/Admin/MediaListTable.php:79 #: includes/Admin/MediaListTable.php:81
msgid "Run External Scan" msgid "Run External Scan"
msgstr "Executar exploració externa" msgstr "Executar exploració externa"
#: includes/Admin/MediaListTable.php:80 #: includes/Admin/MediaListTable.php:82
msgid "Purge External Data" msgid "Purge External Data"
msgstr "Eliminar dades externes" msgstr "Eliminar dades externes"
#: includes/Admin/MediaListTable.php:81 #: includes/Admin/MediaListTable.php:83
msgid "Export CSV" msgid "Export CSV"
msgstr "Exportar CSV" msgstr "Exportar CSV"
#: includes/Admin/MediaListTable.php:165 #: includes/Admin/MediaListTable.php:84
msgid "Export External Results CSV"
msgstr "Exportar CSV de resultats externs"
#: includes/Admin/MediaListTable.php:168
msgid "Edit" msgid "Edit"
msgstr "Edita" msgstr "Edita"
#: includes/Admin/MediaListTable.php:170 #: includes/Admin/MediaListTable.php:173
msgid "View Details" msgid "View Details"
msgstr "Visualitza els detalls" msgstr "Visualitza els detalls"
#: includes/Admin/MediaListTable.php:236 #: includes/Admin/MediaListTable.php:239
msgid "(no title)" msgid "(no title)"
msgstr "(sense títol)" msgstr "(sense títol)"
#. translators: %d: number of additional usages #. translators: %d: number of additional usages
#: includes/Admin/MediaListTable.php:251 #: includes/Admin/MediaListTable.php:254
#, php-format #, php-format
msgid "+ %d more…" msgid "+ %d more…"
msgstr "+ %d més…" msgstr "+ %d més…"
#: includes/Admin/MediaListTable.php:273 #: includes/Admin/MediaListTable.php:276 includes/Admin/ToolsPage.php:580
#: includes/Admin/ToolsPage.php:581 includes/Admin/ToolsPage.php:582
msgid "Pending" msgid "Pending"
msgstr "Pendent" msgstr "Pendent"
#: includes/Admin/MediaListTable.php:274 #: includes/Admin/MediaListTable.php:277
msgid "Queued" msgid "Queued"
msgstr "En cua" msgstr "En cua"
#: includes/Admin/MediaListTable.php:276 #: includes/Admin/MediaListTable.php:279
msgid "Matches Found" msgid "Matches Found"
msgstr "Coincidències trobades" msgstr "Coincidències trobades"
#: includes/Admin/MediaListTable.php:277 #: includes/Admin/MediaListTable.php:280
msgid "Error" msgid "Error"
msgstr "Error" msgstr "Error"
#: includes/Admin/MediaListTable.php:305 #: includes/Admin/MediaListTable.php:308
msgid "View Results" msgid "View Results"
msgstr "Visualitza els resultats" msgstr "Visualitza els resultats"
#: includes/Admin/MediaListTable.php:336 #. translators: %s: human-readable time difference, e.g. "3 days"
#: includes/Admin/MediaListTable.php:350
#, php-format
msgid "%s ago"
msgstr "fa %s"
#: includes/Admin/MediaListTable.php:380
msgid "All statuses" msgid "All statuses"
msgstr "Tots els estats" msgstr "Tots els estats"
#: includes/Admin/MediaListTable.php:350 #: includes/Admin/MediaListTable.php:394
msgid "All post types" msgid "All post types"
msgstr "Tots els tipus d'entrada" msgstr "Tots els tipus d'entrada"
#: includes/Admin/MediaListTable.php:365 #: includes/Admin/MediaListTable.php:409
msgid "Unused only" msgid "Unused only"
msgstr "Només no usades" msgstr "Només no usades"
#: includes/Admin/MediaListTable.php:368 #: includes/Admin/MediaListTable.php:412
msgid "Filter" msgid "Filter"
msgstr "Filtra" msgstr "Filtra"
#: includes/Admin/MediaListTable.php:378 #: includes/Admin/MediaListTable.php:422
msgid "" msgid ""
"No indexed attachments found. Run wp mra scan-internal to populate the index." "No indexed attachments found. Run wp mra scan-internal to populate the index."
msgstr "" msgstr ""
@ -434,6 +466,417 @@ msgstr ""
"Nombre màxim de sol·licituds d'API per minut per proveïdor (160). Per " "Nombre màxim de sol·licituds d'API per minut per proveïdor (160). Per "
"defecte: 10." "defecte: 10."
#: includes/Admin/ToolsPage.php:85
msgid "Done."
msgstr "Fet."
#: includes/Admin/ToolsPage.php:86
msgid "Stopped."
msgstr "Aturat."
#: includes/Admin/ToolsPage.php:87
msgid "An error occurred."
msgstr "S'ha produït un error."
#: includes/Admin/ToolsPage.php:139
msgid "Invalid batch type."
msgstr "Tipus de lot no vàlid."
#: includes/Admin/ToolsPage.php:256
msgid "System Status"
msgstr "Estat del sistema"
#: includes/Admin/ToolsPage.php:259
msgid "Operations"
msgstr "Operacions"
#: includes/Admin/ToolsPage.php:262
msgid "Direct scan runner"
msgstr "Executor d'exploració directa"
#: includes/Admin/ToolsPage.php:264
msgid ""
"Run scan batches directly from your browser without relying on background "
"jobs. Useful when WP-Cron is disabled or Action Scheduler is not running. "
"Keep this tab open while processing."
msgstr ""
"Executeu lots d'exploració directament des del navegador sense dependre de "
"treballs en segon pla. Útil quan WP-Cron està desactivat o Action Scheduler "
"no s'està executant. Manteniu aquesta pestanya oberta mentre es processa."
#: includes/Admin/ToolsPage.php:300
msgid "not installed"
msgstr "no instal·lat"
#: includes/Admin/ToolsPage.php:358
msgid "Internal scan scheduled."
msgstr "Exploració interna programada."
#: includes/Admin/ToolsPage.php:359
msgid "External scan scheduled."
msgstr "Exploració externa programada."
#: includes/Admin/ToolsPage.php:360
msgid "Failed scans re-queued. Background scan scheduled."
msgstr ""
"Exploracions fallides reencolades. Exploració en segon pla "
"programada."
#: includes/Admin/ToolsPage.php:361
msgid "Usage data cleared. Background scan scheduled."
msgstr "Dades d'ús eliminades. Exploració en segon pla programada."
#: includes/Admin/ToolsPage.php:362
msgid "Index cleared. Fresh indexing scheduled."
msgstr "Índex eliminat. Nova indexació programada."
#: includes/Admin/ToolsPage.php:363
msgid "External scan data purged."
msgstr "Dades d'exploració externa eliminades."
#: includes/Admin/ToolsPage.php:364
msgid "All data reset. Fresh indexing scheduled."
msgstr "Totes les dades restablertes. Nova indexació programada."
#: includes/Admin/ToolsPage.php:367
msgid "Operation completed."
msgstr "Operació completada."
#: includes/Admin/ToolsPage.php:411
msgid "Database schema"
msgstr "Esquema de base de dades"
#. translators: %s: version number
#: includes/Admin/ToolsPage.php:417
#, php-format
msgid "v%s up to date"
msgstr "v%s actualitzat"
#. translators: 1: stored version, 2: expected version
#: includes/Admin/ToolsPage.php:423
#, php-format
msgid "Installed: %1$s — Required: %2$s"
msgstr "Instal·lat: %1$s — Requerit: %2$s"
#: includes/Admin/ToolsPage.php:432
msgid "Deactivate and reactivate the plugin to apply pending migrations."
msgstr ""
"Desactiveu i reactiveu el connector per aplicar les migracions "
"pendents."
#: includes/Admin/ToolsPage.php:439
msgid "Action Scheduler"
msgstr "Action Scheduler"
#: includes/Admin/ToolsPage.php:442
msgid "Available"
msgstr "Disponible"
#: includes/Admin/ToolsPage.php:444
msgid "Not available"
msgstr "No disponible"
#: includes/Admin/ToolsPage.php:449
msgid ""
"Install and activate the Action Scheduler plugin. Background scanning will "
"not work without it."
msgstr ""
"Instal·leu i activeu el connector Action Scheduler. L'exploració en segon "
"pla no funcionarà sense ell."
#: includes/Admin/ToolsPage.php:456
msgid "WP-Cron"
msgstr "WP-Cron"
#: includes/Admin/ToolsPage.php:459
msgid "Disabled (DISABLE_WP_CRON)"
msgstr "Desactivat (DISABLE_WP_CRON)"
#: includes/Admin/ToolsPage.php:461
msgid "Enabled"
msgstr "Activat"
#: includes/Admin/ToolsPage.php:466
msgid ""
"Background jobs will not fire automatically. Trigger the cron externally or "
"use WP-CLI:"
msgstr ""
"Els treballs en segon pla no s'executaran automàticament. Activeu el cron "
"externament o useu WP-CLI:"
#: includes/Admin/ToolsPage.php:474
msgid "Internal index"
msgstr "Índex intern"
#. translators: 1: indexed count, 2: total images
#: includes/Admin/ToolsPage.php:479
#, php-format
msgid "%1$s / %2$s images indexed"
msgstr "%1$s / %2$s imatges indexades"
#. translators: %s: formatted count
#: includes/Admin/ToolsPage.php:490
#, php-format
msgid "%s image not yet indexed."
msgid_plural "%s images not yet indexed."
msgstr[0] "%s imatge encara no indexada."
msgstr[1] "%s imatges encara no indexades."
#: includes/Admin/ToolsPage.php:494
msgid "Schedule an internal scan below."
msgstr "Programeu una exploració interna a continuació."
#: includes/Admin/ToolsPage.php:502
msgid "Usage scan"
msgstr "Exploració d'ús"
#. translators: 1: scanned count, 2: total indexed
#: includes/Admin/ToolsPage.php:507
#, php-format
msgid "%1$s / %2$s scanned for usage"
msgstr "%1$s / %2$s explorades per a ús"
#. translators: %s: formatted count
#: includes/Admin/ToolsPage.php:518
#, php-format
msgid "%s pending."
msgid_plural "%s pending."
msgstr[0] "%s pendent."
msgstr[1] "%s pendents."
#: includes/Admin/ToolsPage.php:539 includes/Admin/ToolsPage.php:736
msgid "External scan"
msgstr "Exploració externa"
#. translators: 1: scanned 2: matches 3: queued 4: pending 5: errors
#: includes/Admin/ToolsPage.php:544
#, php-format
msgid "%1$s scanned · %2$s matches · %3$s queued · %4$s pending · %5$s errors"
msgstr ""
"%1$s explorades · %2$s coincidències · %3$s en cua · %4$s pendents · "
"%5$s errors"
#: includes/Admin/ToolsPage.php:555
msgid ""
"Some scans failed. Check that API keys are configured correctly in Settings."
msgstr ""
"Algunes exploracions han fallat. Comproveu que les claus d'API estan "
"configurades correctament a la Configuració."
#: includes/Admin/ToolsPage.php:557
msgid "Schedule an external scan below to process pending items."
msgstr ""
"Programeu una exploració externa a continuació per processar els elements "
"pendents."
#: includes/Admin/ToolsPage.php:575
msgid "Scheduled jobs"
msgstr "Treballs programats"
#: includes/Admin/ToolsPage.php:578
msgid "Action Scheduler not available."
msgstr "Action Scheduler no disponible."
#: includes/Admin/ToolsPage.php:580
msgid "Index:"
msgstr "Índex:"
#: includes/Admin/ToolsPage.php:581
msgid "Usage:"
msgstr "Ús:"
#: includes/Admin/ToolsPage.php:582
msgid "External:"
msgstr "Extern:"
#: includes/Admin/ToolsPage.php:587
msgid ""
"There is work to do but no background job is scheduled. Use the operations "
"below."
msgstr ""
"Hi ha feina pendent però no hi ha cap treball en segon pla programat. "
"Useu les operacions de sota."
#: includes/Admin/ToolsPage.php:589
msgid "WP-Cron is disabled. Jobs will not fire automatically."
msgstr ""
"WP-Cron està desactivat. Els treballs no s'executaran "
"automàticament."
#: includes/Admin/ToolsPage.php:599
msgid "WP-CLI commands to process scans manually:"
msgstr "Comandes WP-CLI per processar exploracions manualment:"
#: includes/Admin/ToolsPage.php:616
msgid "Schedule internal scan"
msgstr "Programar exploració interna"
#: includes/Admin/ToolsPage.php:617
msgid ""
"Queue a background job to index all unindexed media files and scan their "
"usage across posts, pages, and custom fields. Safe to run at any time — will "
"not re-process completed work."
msgstr ""
"Posa en cua un treball en segon pla per indexar tots els fitxers multimèdia "
"no indexats i explorar el seu ús en entrades, pàgines i camps personalitzats. "
"Es pot executar en qualsevol moment: no reprocessarà el treball ja completat."
#: includes/Admin/ToolsPage.php:623
msgid "Schedule external scan"
msgstr "Programar exploració externa"
#: includes/Admin/ToolsPage.php:624
msgid ""
"Mark all pending attachments as queued and launch a background reverse-image-"
"search via the configured providers. Requires API keys in Settings."
msgstr ""
"Marca tots els fitxers adjunts pendents com en cua i llança una cerca inversa "
"d'imatges en segon pla a través dels proveïdors configurats. Requereix claus "
"d'API a la Configuració."
#: includes/Admin/ToolsPage.php:630
msgid "Requeue scan errors"
msgstr "Reencolar errors d'exploració"
#: includes/Admin/ToolsPage.php:631
msgid ""
"Re-queues only the attachments that failed with an error on the last "
"external scan run. Does not affect already-scanned items. Useful to retry "
"after fixing API key issues."
msgstr ""
"Reencola només els fitxers adjunts que van fallar amb un error en l'última "
"execució de l'exploració externa. No afecta els elements ja explorats. "
"Útil per reintentar després de corregir problemes amb les claus d'API."
#: includes/Admin/ToolsPage.php:640
msgid "Reset usage data"
msgstr "Restablir dades d'ús"
#: includes/Admin/ToolsPage.php:641
msgid ""
"Clears all usage rows and resets the usage-scan flag for every indexed "
"attachment. File metadata in the index is preserved. A background usage scan "
"is scheduled immediately after."
msgstr ""
"Elimina totes les files d'ús i restableix l'indicador d'exploració d'ús per "
"a cada fitxer adjunt indexat. Els metadades del fitxer a l'índex es "
"conserven. Es programa una exploració en segon pla immediatament després."
#: includes/Admin/ToolsPage.php:643
msgid "This will delete all usage data. Continue?"
msgstr "Això eliminarà totes les dades d'ús. Voleu continuar?"
#: includes/Admin/ToolsPage.php:647
msgid "Re-index media library"
msgstr "Re-indexar la biblioteca de mitjans"
#: includes/Admin/ToolsPage.php:648
msgid ""
"Deletes all index and usage entries, then schedules a full fresh re-index. "
"External scan results are not affected. Useful if many media files have been "
"added or removed."
msgstr ""
"Elimina totes les entrades de l'índex i d'ús, i després programa una "
"re-indexació completa. Els resultats de l'exploració externa no es veuen "
"afectats. Útil si s'han afegit o eliminat molts fitxers multimèdia."
#: includes/Admin/ToolsPage.php:650
msgid "This will delete all index and usage data. Continue?"
msgstr "Això eliminarà totes les dades de l'índex i d'ús. Voleu continuar?"
#: includes/Admin/ToolsPage.php:654
msgid "Purge external data"
msgstr "Purgar dades externes"
#: includes/Admin/ToolsPage.php:655
msgid ""
"Deletes all external scan results and resets every attachment's external "
"status to pending. Use this before switching API providers or to force a "
"complete re-scan."
msgstr ""
"Elimina tots els resultats de l'exploració externa i restableix l'estat "
"extern de cada fitxer adjunt a pendent. Useu-ho abans de canviar de "
"proveïdors d'API o per forçar una exploració completa."
#: includes/Admin/ToolsPage.php:657
msgid "This will delete all external scan results. Continue?"
msgstr ""
"Això eliminarà tots els resultats de l'exploració externa. Voleu "
"continuar?"
#: includes/Admin/ToolsPage.php:661
msgid "Reset all data"
msgstr "Restablir totes les dades"
#: includes/Admin/ToolsPage.php:662
msgid ""
"Truncates all three plugin tables (index, usage, external results) and "
"schedules a fresh indexing run. Use this to start completely from scratch."
msgstr ""
"Buida les tres taules del connector (índex, ús, resultats externs) i "
"programa una nova indexació. Useu-ho per començar completament des de zero."
#: includes/Admin/ToolsPage.php:664
msgid ""
"This will permanently delete ALL plugin data and cannot be undone. Are you "
"absolutely sure?"
msgstr ""
"Això eliminarà TOTES les dades del connector de forma permanent i no es pot "
"desfer. Esteu absolutament segurs?"
#: includes/Admin/ToolsPage.php:675
msgid "Destructive operations"
msgstr "Operacions destructives"
#: includes/Admin/ToolsPage.php:678
msgid ""
"These operations permanently delete plugin data. Intended for forced "
"rescans, troubleshooting, or starting fresh. A confirmation prompt will "
"appear before any action is taken."
msgstr ""
"Aquestes operacions eliminen les dades del connector de forma permanent. "
"Pensades per forçar re-exploracions, solucionar problemes o començar des de "
"zero. Apareixerà una confirmació abans d'executar qualsevol acció."
#: includes/Admin/ToolsPage.php:726
msgid "Index media files"
msgstr "Indexar fitxers multimèdia"
#: includes/Admin/ToolsPage.php:727
msgid ""
"Discovers unindexed images and registers them in the plugin index. Run this "
"first."
msgstr ""
"Descobreix imatges no indexades i les registra a l'índex del connector. "
"Executeu-ho primer."
#: includes/Admin/ToolsPage.php:731
msgid "Scan usage"
msgstr "Explorar l'ús"
#: includes/Admin/ToolsPage.php:732
msgid ""
"Checks every indexed image for usage across posts, pages, and custom fields."
msgstr ""
"Comprova l'ús de cada imatge indexada en entrades, pàgines i camps "
"personalitzats."
#: includes/Admin/ToolsPage.php:737
msgid ""
"Sends images to external providers for reverse-image search. Requires API "
"keys in Settings."
msgstr ""
"Envia imatges a proveïdors externs per a la cerca inversa d'imatges. "
"Requereix claus d'API a la Configuració."
#: includes/Admin/ToolsPage.php:754
msgid "Run now"
msgstr "Executa ara"
#: includes/Admin/ToolsPage.php:757
msgid "Stop"
msgstr "Atura"
#. translators: %s: DB version number #. translators: %s: DB version number
#: includes/CLI/Command.php:49 #: includes/CLI/Command.php:49
#, php-format #, php-format
@ -494,7 +937,11 @@ msgstr "S'estan executant les exploracions externes…"
msgid "External scan complete. %d attachments processed." msgid "External scan complete. %d attachments processed."
msgstr "Exploració externa completada. %d fitxers adjunts processats." msgstr "Exploració externa completada. %d fitxers adjunts processats."
#: includes/Core/Plugin.php:112 includes/Core/Plugin.php:113 #: includes/Core/Plugin.php:122 includes/Core/Plugin.php:123
msgid "Tools"
msgstr "Eines"
#: includes/Core/Plugin.php:135 includes/Core/Plugin.php:136
msgid "Settings" msgid "Settings"
msgstr "Configuració" msgstr "Configuració"
@ -522,14 +969,15 @@ msgstr "Última exploració (interna)"
msgid "Last Scanned (external)" msgid "Last Scanned (external)"
msgstr "Última exploració (externa)" msgstr "Última exploració (externa)"
#: includes/Privacy/DataExporter.php:303
msgid "Provider"
msgstr "Proveïdor"
#: includes/Privacy/DataExporter.php:307
msgid "Match Count"
msgstr "Nombre de coincidències"
#: includes/Privacy/DataExporter.php:311 #: includes/Privacy/DataExporter.php:311
msgid "Scanned At" msgid "Scanned At"
msgstr "Explorat el" msgstr "Explorat el"
#~ msgid "https://robotstxt.es/plugins/media-audit/"
#~ msgstr "https://robotstxt.es/plugins/media-audit/"
#~ msgid "Javier Casares"
#~ msgstr "Javier Casares"
#~ msgid "https://javiercasares.com"
#~ msgstr "https://javiercasares.com"

View file

@ -2,10 +2,10 @@
# This file is distributed under the GPL-3.0-or-later. # This file is distributed under the GPL-3.0-or-later.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Media Audit (by ROBOTSTXT) 1.0.0\n" "Project-Id-Version: Media Audit (by ROBOTSTXT) 1.2.0\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-"
"mediaaudit\n" "mediaaudit\n"
"POT-Creation-Date: 2026-05-02T06:06:40+00:00\n" "POT-Creation-Date: 2026-05-02T07:36:54+00:00\n"
"PO-Revision-Date: 2026-05-01 07:41+0000\n" "PO-Revision-Date: 2026-05-01 07:41+0000\n"
"Last-Translator: Javier Casares <javier@casares.org>\n" "Last-Translator: Javier Casares <javier@casares.org>\n"
"Language-Team: Spanish (Spain) <es@li.org>\n" "Language-Team: Spanish (Spain) <es@li.org>\n"
@ -23,8 +23,8 @@ msgstr "Auditoría de medios (by ROBOTSTXT)"
#. Plugin URI of the plugin #. Plugin URI of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "https://robotstxt.es/plugins/media-audit/" msgid "https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit"
msgstr "https://robotstxt.es/plugins/media-audit/" msgstr ""
#. Description of the plugin #. Description of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
@ -37,279 +37,311 @@ msgstr ""
#. Author of the plugin #. Author of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "Javier Casares" msgid "ROBOTSTXT"
msgstr "Javier Casares" msgstr ""
#. Author URI of the plugin #. Author URI of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "https://javiercasares.com" msgid "https://www.robotstxt.es/"
msgstr "https://javiercasares.com" msgstr ""
#: includes/Admin/AuditPage.php:112 #: includes/Admin/AuditPage.php:114
msgid "Loading…" msgid "Loading…"
msgstr "Cargando…" msgstr "Cargando…"
#: includes/Admin/AuditPage.php:113 #: includes/Admin/AuditPage.php:115
msgid "Error loading details." msgid "Error loading details."
msgstr "Error al cargar los detalles." msgstr "Error al cargar los detalles."
#: includes/Admin/AuditPage.php:114 includes/Admin/AuditPage.php:190 #: includes/Admin/AuditPage.php:116 includes/Admin/AuditPage.php:192
msgid "This image is not used in any post." msgid "This image is not used in any post."
msgstr "Esta imagen no se usa en ninguna entrada." msgstr "Esta imagen no se usa en ninguna entrada."
#: includes/Admin/AuditPage.php:115 includes/Admin/AuditPage.php:152 #: includes/Admin/AuditPage.php:117 includes/Admin/AuditPage.php:154
msgid "Usage Details" msgid "Usage Details"
msgstr "Detalles de uso" msgstr "Detalles de uso"
#: includes/Admin/AuditPage.php:131 includes/Admin/Settings.php:325 #: includes/Admin/AuditPage.php:133 includes/Admin/Settings.php:325
#: includes/Admin/ToolsPage.php:245
msgid "You do not have permission to access this page." msgid "You do not have permission to access this page."
msgstr "No tienes permiso para acceder a esta página." msgstr "No tienes permiso para acceder a esta página."
#: includes/Admin/AuditPage.php:138 includes/Core/Plugin.php:89 #: includes/Admin/AuditPage.php:140 includes/Core/Plugin.php:99
#: includes/Core/Plugin.php:90 includes/Core/Plugin.php:103 #: includes/Core/Plugin.php:100 includes/Core/Plugin.php:113
#: includes/Core/Plugin.php:104 includes/Privacy/DataEraser.php:33 #: includes/Core/Plugin.php:114 includes/Privacy/DataEraser.php:33
#: includes/Privacy/DataExporter.php:45 #: includes/Privacy/DataExporter.php:45
msgid "Media Audit" msgid "Media Audit"
msgstr "Auditoría de medios" msgstr "Auditoría de medios"
#: includes/Admin/AuditPage.php:144 #: includes/Admin/AuditPage.php:146
msgid "Search" msgid "Search"
msgstr "Buscar" msgstr "Buscar"
#: includes/Admin/AuditPage.php:153 #: includes/Admin/AuditPage.php:155
msgid "Close" msgid "Close"
msgstr "Cerrar" msgstr "Cerrar"
#: includes/Admin/AuditPage.php:171 includes/Admin/AuditPage.php:459 #: includes/Admin/AuditPage.php:173 includes/Admin/AuditPage.php:461
#: includes/Admin/AuditPage.php:510 includes/Admin/AuditPage.php:542 #: includes/Admin/AuditPage.php:515 includes/Admin/AuditPage.php:648
#: includes/Admin/AuditPage.php:680 includes/Admin/ToolsPage.php:105
#: includes/Admin/ToolsPage.php:161
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "Permisos insuficientes." msgstr "Permisos insuficientes."
#: includes/Admin/AuditPage.php:176 #: includes/Admin/AuditPage.php:178
msgid "Invalid attachment ID." msgid "Invalid attachment ID."
msgstr "ID de adjunto no válido." msgstr "ID de adjunto no válido."
#: includes/Admin/AuditPage.php:184 #: includes/Admin/AuditPage.php:186
msgid "Featured Image" msgid "Featured Image"
msgstr "Imagen destacada" msgstr "Imagen destacada"
#: includes/Admin/AuditPage.php:185 #: includes/Admin/AuditPage.php:187
msgid "Post Content" msgid "Post Content"
msgstr "Contenido de la entrada" msgstr "Contenido de la entrada"
#: includes/Admin/AuditPage.php:186 #: includes/Admin/AuditPage.php:188
msgid "Custom Field" msgid "Custom Field"
msgstr "Campo personalizado" msgstr "Campo personalizado"
#: includes/Admin/AuditPage.php:194 #: includes/Admin/AuditPage.php:196
msgid "Post" msgid "Post"
msgstr "Entrada" msgstr "Entrada"
#: includes/Admin/AuditPage.php:195 #: includes/Admin/AuditPage.php:197
msgid "Type" msgid "Type"
msgstr "Tipo" msgstr "Tipo"
#: includes/Admin/AuditPage.php:196 #: includes/Admin/AuditPage.php:198
msgid "Context" msgid "Context"
msgstr "Contexto" msgstr "Contexto"
#: includes/Admin/AuditPage.php:197 #: includes/Admin/AuditPage.php:199
msgid "Status" msgid "Status"
msgstr "Estado" msgstr "Estado"
#: includes/Admin/AuditPage.php:240 #: includes/Admin/AuditPage.php:242
msgid "Internal Usage" msgid "Internal Usage"
msgstr "Uso interno" msgstr "Uso interno"
#: includes/Admin/AuditPage.php:288 #: includes/Admin/AuditPage.php:290
msgid "External Scan Results" msgid "External Scan Results"
msgstr "Resultados del escaneo externo" msgstr "Resultados del escaneo externo"
#. translators: %d: number of external matches #. translators: %d: number of external matches
#: includes/Admin/AuditPage.php:312 #: includes/Admin/AuditPage.php:314
#, php-format #, php-format
msgid "%d match" msgid "%d match"
msgid_plural "%d matches" msgid_plural "%d matches"
msgstr[0] "%d coincidencia" msgstr[0] "%d coincidencia"
msgstr[1] "%d coincidencias" msgstr[1] "%d coincidencias"
#: includes/Admin/AuditPage.php:317 #: includes/Admin/AuditPage.php:319
msgid "No matches" msgid "No matches"
msgstr "Sin coincidencias" msgstr "Sin coincidencias"
#: includes/Admin/AuditPage.php:328 #: includes/Admin/AuditPage.php:330
msgid "Domain" msgid "Domain"
msgstr "Dominio" msgstr "Dominio"
#: includes/Admin/AuditPage.php:329 #: includes/Admin/AuditPage.php:331
msgid "Occurrences" msgid "Occurrences"
msgstr "Apariciones" msgstr "Apariciones"
#: includes/Admin/AuditPage.php:350 #: includes/Admin/AuditPage.php:352
msgid "Consensus Domains" msgid "Consensus Domains"
msgstr "Dominios de consenso" msgstr "Dominios de consenso"
#. translators: %d: number of providers that agree #. translators: %d: number of providers that agree
#: includes/Admin/AuditPage.php:356 #: includes/Admin/AuditPage.php:358
#, php-format #, php-format
msgid "%d provider" msgid "%d provider"
msgid_plural "%d providers" msgid_plural "%d providers"
msgstr[0] "%d proveedor" msgstr[0] "%d proveedor"
msgstr[1] "%d proveedores" msgstr[1] "%d proveedores"
#: includes/Admin/AuditPage.php:388 #: includes/Admin/AuditPage.php:390
msgid "In Index" msgid "In Index"
msgstr "En el índice" msgstr "En el índice"
#. translators: %d: count of images not yet indexed #. translators: %d: count of images not yet indexed
#: includes/Admin/AuditPage.php:392 #: includes/Admin/AuditPage.php:394
#, php-format #, php-format
msgid "%d not yet indexed" msgid "%d not yet indexed"
msgstr "%d aún no indexadas" msgstr "%d aún no indexadas"
#: includes/Admin/AuditPage.php:395 #: includes/Admin/AuditPage.php:397
msgid "All indexed" msgid "All indexed"
msgstr "Todas indexadas" msgstr "Todas indexadas"
#: includes/Admin/AuditPage.php:400 includes/Admin/MediaListTable.php:275 #: includes/Admin/AuditPage.php:402 includes/Admin/MediaListTable.php:278
msgid "Scanned" msgid "Scanned"
msgstr "Escaneadas" msgstr "Escaneadas"
#. translators: %d: percentage #. translators: %d: percentage
#: includes/Admin/AuditPage.php:403 #: includes/Admin/AuditPage.php:405
#, php-format #, php-format
msgid "%d%% of index" msgid "%d%% of index"
msgstr "%d%% del índice" msgstr "%d%% del índice"
#: includes/Admin/AuditPage.php:410 #: includes/Admin/AuditPage.php:412
msgid "Used" msgid "Used"
msgstr "Usadas" msgstr "Usadas"
#: includes/Admin/AuditPage.php:416 includes/Admin/MediaListTable.php:200 #: includes/Admin/AuditPage.php:418 includes/Admin/MediaListTable.php:203
msgid "Unused" msgid "Unused"
msgstr "Sin usar" msgstr "Sin usar"
#: includes/Admin/AuditPage.php:423 #: includes/Admin/AuditPage.php:425
msgid "External Matches" msgid "External Matches"
msgstr "Coincidencias externas" msgstr "Coincidencias externas"
#: includes/Admin/AuditPage.php:473 #: includes/Admin/AuditPage.php:475 includes/Admin/AuditPage.php:529
msgid "Could not open output stream." msgid "Could not open output stream."
msgstr "No se pudo abrir el flujo de salida." msgstr "No se pudo abrir el flujo de salida."
#: includes/Admin/AuditPage.php:482 includes/Privacy/DataExporter.php:257 #: includes/Admin/AuditPage.php:484 includes/Admin/AuditPage.php:537
#: includes/Privacy/DataExporter.php:299 #: includes/Privacy/DataExporter.php:257 includes/Privacy/DataExporter.php:299
msgid "Attachment ID" msgid "Attachment ID"
msgstr "ID del adjunto" msgstr "ID del adjunto"
#: includes/Admin/AuditPage.php:483 includes/Admin/MediaListTable.php:52 #: includes/Admin/AuditPage.php:485 includes/Admin/AuditPage.php:538
#: includes/Privacy/DataExporter.php:261 #: includes/Admin/MediaListTable.php:52 includes/Privacy/DataExporter.php:261
msgid "Filename" msgid "Filename"
msgstr "Nombre de archivo" msgstr "Nombre de archivo"
#: includes/Admin/AuditPage.php:484 includes/Privacy/DataExporter.php:265 #: includes/Admin/AuditPage.php:486 includes/Admin/AuditPage.php:539
#: includes/Privacy/DataExporter.php:265
msgid "File URL" msgid "File URL"
msgstr "URL del archivo" msgstr "URL del archivo"
#: includes/Admin/AuditPage.php:485 #: includes/Admin/AuditPage.php:487
msgid "MIME Type" msgid "MIME Type"
msgstr "Tipo MIME" msgstr "Tipo MIME"
#: includes/Admin/AuditPage.php:486 #: includes/Admin/AuditPage.php:488
msgid "File Size (bytes)" msgid "File Size (bytes)"
msgstr "Tamaño del archivo (bytes)" msgstr "Tamaño del archivo (bytes)"
#: includes/Admin/AuditPage.php:487 includes/Admin/MediaListTable.php:56 #: includes/Admin/AuditPage.php:489 includes/Admin/AuditPage.php:540
#: includes/Admin/MediaListTable.php:56
msgid "External Status" msgid "External Status"
msgstr "Estado externo" msgstr "Estado externo"
#: includes/Admin/AuditPage.php:488 includes/Admin/MediaListTable.php:54 #: includes/Admin/AuditPage.php:490 includes/Admin/MediaListTable.php:54
msgid "Usage Count" msgid "Usage Count"
msgstr "Número de usos" msgstr "Número de usos"
#: includes/Admin/AuditPage.php:489 #: includes/Admin/AuditPage.php:491
msgid "Used In (post titles)" msgid "Used In (post titles)"
msgstr "Usado en (títulos de entradas)" msgstr "Usado en (títulos de entradas)"
#: includes/Admin/AuditPage.php:541 includes/Admin/MediaListTable.php:57
msgid "Last Scanned"
msgstr "Último escaneo"
#: includes/Admin/AuditPage.php:542 includes/Privacy/DataExporter.php:303
msgid "Provider"
msgstr "Proveedor"
#: includes/Admin/AuditPage.php:543 includes/Privacy/DataExporter.php:307
msgid "Match Count"
msgstr "Número de coincidencias"
#: includes/Admin/AuditPage.php:544
msgid "Top Domains"
msgstr "Dominios principales"
#: includes/Admin/MediaListTable.php:51 #: includes/Admin/MediaListTable.php:51
msgid "Thumbnail" msgid "Thumbnail"
msgstr "Miniatura" msgstr "Miniatura"
#: includes/Admin/MediaListTable.php:53 #: includes/Admin/MediaListTable.php:53
msgid "ID" msgid "ID"
msgstr "" msgstr "ID"
#: includes/Admin/MediaListTable.php:55 #: includes/Admin/MediaListTable.php:55
msgid "Usages" msgid "Usages"
msgstr "Usos" msgstr "Usos"
#: includes/Admin/MediaListTable.php:79 #: includes/Admin/MediaListTable.php:81
msgid "Run External Scan" msgid "Run External Scan"
msgstr "Ejecutar escaneo externo" msgstr "Ejecutar escaneo externo"
#: includes/Admin/MediaListTable.php:80 #: includes/Admin/MediaListTable.php:82
msgid "Purge External Data" msgid "Purge External Data"
msgstr "Purgar datos externos" msgstr "Purgar datos externos"
#: includes/Admin/MediaListTable.php:81 #: includes/Admin/MediaListTable.php:83
msgid "Export CSV" msgid "Export CSV"
msgstr "Exportar CSV" msgstr "Exportar CSV"
#: includes/Admin/MediaListTable.php:165 #: includes/Admin/MediaListTable.php:84
msgid "Export External Results CSV"
msgstr "Exportar CSV de resultados externos"
#: includes/Admin/MediaListTable.php:168
msgid "Edit" msgid "Edit"
msgstr "Editar" msgstr "Editar"
#: includes/Admin/MediaListTable.php:170 #: includes/Admin/MediaListTable.php:173
msgid "View Details" msgid "View Details"
msgstr "Ver detalles" msgstr "Ver detalles"
#: includes/Admin/MediaListTable.php:236 #: includes/Admin/MediaListTable.php:239
msgid "(no title)" msgid "(no title)"
msgstr "(sin título)" msgstr "(sin título)"
#. translators: %d: number of additional usages #. translators: %d: number of additional usages
#: includes/Admin/MediaListTable.php:251 #: includes/Admin/MediaListTable.php:254
#, php-format #, php-format
msgid "+ %d more…" msgid "+ %d more…"
msgstr "+ %d más…" msgstr "+ %d más…"
#: includes/Admin/MediaListTable.php:273 #: includes/Admin/MediaListTable.php:276 includes/Admin/ToolsPage.php:580
#: includes/Admin/ToolsPage.php:581 includes/Admin/ToolsPage.php:582
msgid "Pending" msgid "Pending"
msgstr "Pendiente" msgstr "Pendiente"
#: includes/Admin/MediaListTable.php:274 #: includes/Admin/MediaListTable.php:277
msgid "Queued" msgid "Queued"
msgstr "En cola" msgstr "En cola"
#: includes/Admin/MediaListTable.php:276 #: includes/Admin/MediaListTable.php:279
msgid "Matches Found" msgid "Matches Found"
msgstr "Coincidencias encontradas" msgstr "Coincidencias encontradas"
#: includes/Admin/MediaListTable.php:277 #: includes/Admin/MediaListTable.php:280
msgid "Error" msgid "Error"
msgstr "Error" msgstr "Error"
#: includes/Admin/MediaListTable.php:305 #: includes/Admin/MediaListTable.php:308
msgid "View Results" msgid "View Results"
msgstr "Ver resultados" msgstr "Ver resultados"
#: includes/Admin/MediaListTable.php:336 #. translators: %s: human-readable time difference, e.g. "3 days"
#: includes/Admin/MediaListTable.php:350
#, php-format
msgid "%s ago"
msgstr "hace %s"
#: includes/Admin/MediaListTable.php:380
msgid "All statuses" msgid "All statuses"
msgstr "Todos los estados" msgstr "Todos los estados"
#: includes/Admin/MediaListTable.php:350 #: includes/Admin/MediaListTable.php:394
msgid "All post types" msgid "All post types"
msgstr "Todos los tipos de entrada" msgstr "Todos los tipos de entrada"
#: includes/Admin/MediaListTable.php:365 #: includes/Admin/MediaListTable.php:409
msgid "Unused only" msgid "Unused only"
msgstr "Solo sin usar" msgstr "Solo sin usar"
#: includes/Admin/MediaListTable.php:368 #: includes/Admin/MediaListTable.php:412
msgid "Filter" msgid "Filter"
msgstr "Filtrar" msgstr "Filtrar"
#: includes/Admin/MediaListTable.php:378 #: includes/Admin/MediaListTable.php:422
msgid "" msgid ""
"No indexed attachments found. Run wp mra scan-internal to populate the index." "No indexed attachments found. Run wp mra scan-internal to populate the index."
msgstr "" msgstr ""
@ -435,6 +467,412 @@ msgstr ""
"Máximo de solicitudes de API por minuto por proveedor (160). " "Máximo de solicitudes de API por minuto por proveedor (160). "
"Predeterminado: 10." "Predeterminado: 10."
#: includes/Admin/ToolsPage.php:85
msgid "Done."
msgstr "Listo."
#: includes/Admin/ToolsPage.php:86
msgid "Stopped."
msgstr "Detenido."
#: includes/Admin/ToolsPage.php:87
msgid "An error occurred."
msgstr "Se ha producido un error."
#: includes/Admin/ToolsPage.php:139
msgid "Invalid batch type."
msgstr "Tipo de lote no válido."
#: includes/Admin/ToolsPage.php:256
msgid "System Status"
msgstr "Estado del sistema"
#: includes/Admin/ToolsPage.php:259
msgid "Operations"
msgstr "Operaciones"
#: includes/Admin/ToolsPage.php:262
msgid "Direct scan runner"
msgstr "Ejecutor de escaneo directo"
#: includes/Admin/ToolsPage.php:264
msgid ""
"Run scan batches directly from your browser without relying on background "
"jobs. Useful when WP-Cron is disabled or Action Scheduler is not running. "
"Keep this tab open while processing."
msgstr ""
"Ejecuta lotes de escaneo directamente desde tu navegador sin depender de "
"trabajos en segundo plano. Útil cuando WP-Cron está desactivado o Action "
"Scheduler no está en ejecución. Mantén esta pestaña abierta mientras se "
"procesa."
#: includes/Admin/ToolsPage.php:300
msgid "not installed"
msgstr "no instalado"
#: includes/Admin/ToolsPage.php:358
msgid "Internal scan scheduled."
msgstr "Escaneo interno programado."
#: includes/Admin/ToolsPage.php:359
msgid "External scan scheduled."
msgstr "Escaneo externo programado."
#: includes/Admin/ToolsPage.php:360
msgid "Failed scans re-queued. Background scan scheduled."
msgstr "Escaneos fallidos reencolados. Escaneo en segundo plano programado."
#: includes/Admin/ToolsPage.php:361
msgid "Usage data cleared. Background scan scheduled."
msgstr "Datos de uso eliminados. Escaneo en segundo plano programado."
#: includes/Admin/ToolsPage.php:362
msgid "Index cleared. Fresh indexing scheduled."
msgstr "Índice eliminado. Re-indexación programada."
#: includes/Admin/ToolsPage.php:363
msgid "External scan data purged."
msgstr "Datos de escaneo externo eliminados."
#: includes/Admin/ToolsPage.php:364
msgid "All data reset. Fresh indexing scheduled."
msgstr "Todos los datos restablecidos. Re-indexación programada."
#: includes/Admin/ToolsPage.php:367
msgid "Operation completed."
msgstr "Operación completada."
#: includes/Admin/ToolsPage.php:411
msgid "Database schema"
msgstr "Esquema de base de datos"
#. translators: %s: version number
#: includes/Admin/ToolsPage.php:417
#, php-format
msgid "v%s up to date"
msgstr "v%s actualizado"
#. translators: 1: stored version, 2: expected version
#: includes/Admin/ToolsPage.php:423
#, php-format
msgid "Installed: %1$s — Required: %2$s"
msgstr "Instalado: %1$s — Requerido: %2$s"
#: includes/Admin/ToolsPage.php:432
msgid "Deactivate and reactivate the plugin to apply pending migrations."
msgstr ""
"Desactiva y reactiva el plugin para aplicar las migraciones pendientes."
#: includes/Admin/ToolsPage.php:439
msgid "Action Scheduler"
msgstr "Action Scheduler"
#: includes/Admin/ToolsPage.php:442
msgid "Available"
msgstr "Disponible"
#: includes/Admin/ToolsPage.php:444
msgid "Not available"
msgstr "No disponible"
#: includes/Admin/ToolsPage.php:449
msgid ""
"Install and activate the Action Scheduler plugin. Background scanning will "
"not work without it."
msgstr ""
"Instala y activa el plugin Action Scheduler. El escaneo en segundo plano no "
"funcionará sin él."
#: includes/Admin/ToolsPage.php:456
msgid "WP-Cron"
msgstr "WP-Cron"
#: includes/Admin/ToolsPage.php:459
msgid "Disabled (DISABLE_WP_CRON)"
msgstr "Desactivado (DISABLE_WP_CRON)"
#: includes/Admin/ToolsPage.php:461
msgid "Enabled"
msgstr "Activado"
#: includes/Admin/ToolsPage.php:466
msgid ""
"Background jobs will not fire automatically. Trigger the cron externally or "
"use WP-CLI:"
msgstr ""
"Los trabajos en segundo plano no se ejecutarán automáticamente. Activa el "
"cron externamente o usa WP-CLI:"
#: includes/Admin/ToolsPage.php:474
msgid "Internal index"
msgstr "Índice interno"
#. translators: 1: indexed count, 2: total images
#: includes/Admin/ToolsPage.php:479
#, php-format
msgid "%1$s / %2$s images indexed"
msgstr "%1$s / %2$s imágenes indexadas"
#. translators: %s: formatted count
#: includes/Admin/ToolsPage.php:490
#, php-format
msgid "%s image not yet indexed."
msgid_plural "%s images not yet indexed."
msgstr[0] "%s imagen aún no indexada."
msgstr[1] "%s imágenes aún no indexadas."
#: includes/Admin/ToolsPage.php:494
msgid "Schedule an internal scan below."
msgstr "Programa un escaneo interno más abajo."
#: includes/Admin/ToolsPage.php:502
msgid "Usage scan"
msgstr "Escaneo de uso"
#. translators: 1: scanned count, 2: total indexed
#: includes/Admin/ToolsPage.php:507
#, php-format
msgid "%1$s / %2$s scanned for usage"
msgstr "%1$s / %2$s escaneadas de uso"
#. translators: %s: formatted count
#: includes/Admin/ToolsPage.php:518
#, php-format
msgid "%s pending."
msgid_plural "%s pending."
msgstr[0] "%s pendiente."
msgstr[1] "%s pendientes."
#: includes/Admin/ToolsPage.php:539 includes/Admin/ToolsPage.php:736
msgid "External scan"
msgstr "Escaneo externo"
#. translators: 1: scanned 2: matches 3: queued 4: pending 5: errors
#: includes/Admin/ToolsPage.php:544
#, php-format
msgid "%1$s scanned · %2$s matches · %3$s queued · %4$s pending · %5$s errors"
msgstr ""
"%1$s escaneadas · %2$s coincidencias · %3$s en cola · %4$s pendientes · %5$s "
"errores"
#: includes/Admin/ToolsPage.php:555
msgid ""
"Some scans failed. Check that API keys are configured correctly in Settings."
msgstr ""
"Algunos escaneos fallaron. Verifica que las claves de API están configuradas "
"correctamente en Ajustes."
#: includes/Admin/ToolsPage.php:557
msgid "Schedule an external scan below to process pending items."
msgstr ""
"Programa un escaneo externo más abajo para procesar los elementos pendientes."
#: includes/Admin/ToolsPage.php:575
msgid "Scheduled jobs"
msgstr "Trabajos programados"
#: includes/Admin/ToolsPage.php:578
msgid "Action Scheduler not available."
msgstr "Action Scheduler no disponible."
#: includes/Admin/ToolsPage.php:580
msgid "Index:"
msgstr "Índice:"
#: includes/Admin/ToolsPage.php:581
msgid "Usage:"
msgstr "Uso:"
#: includes/Admin/ToolsPage.php:582
msgid "External:"
msgstr "Externo:"
#: includes/Admin/ToolsPage.php:587
msgid ""
"There is work to do but no background job is scheduled. Use the operations "
"below."
msgstr ""
"Hay trabajo pendiente pero no hay ningún trabajo en segundo plano "
"programado. Usa las operaciones de abajo."
#: includes/Admin/ToolsPage.php:589
msgid "WP-Cron is disabled. Jobs will not fire automatically."
msgstr ""
"WP-Cron está desactivado. Los trabajos no se ejecutarán automáticamente."
#: includes/Admin/ToolsPage.php:599
msgid "WP-CLI commands to process scans manually:"
msgstr "Comandos WP-CLI para procesar escaneos manualmente:"
#: includes/Admin/ToolsPage.php:616
msgid "Schedule internal scan"
msgstr "Programar escaneo interno"
#: includes/Admin/ToolsPage.php:617
msgid ""
"Queue a background job to index all unindexed media files and scan their "
"usage across posts, pages, and custom fields. Safe to run at any time — will "
"not re-process completed work."
msgstr ""
"Cola un trabajo en segundo plano para indexar todos los archivos multimedia "
"no indexados y escanear su uso en entradas, páginas y campos personalizados. "
"Es seguro ejecutarlo en cualquier momento: no reprocesará el trabajo ya "
"completado."
#: includes/Admin/ToolsPage.php:623
msgid "Schedule external scan"
msgstr "Programar escaneo externo"
#: includes/Admin/ToolsPage.php:624
msgid ""
"Mark all pending attachments as queued and launch a background reverse-image-"
"search via the configured providers. Requires API keys in Settings."
msgstr ""
"Marca todos los adjuntos pendientes como en cola y lanza una búsqueda "
"inversa de imágenes en segundo plano a través de los proveedores "
"configurados. Requiere claves de API en Ajustes."
#: includes/Admin/ToolsPage.php:630
msgid "Requeue scan errors"
msgstr "Reencolar errores de escaneo"
#: includes/Admin/ToolsPage.php:631
msgid ""
"Re-queues only the attachments that failed with an error on the last "
"external scan run. Does not affect already-scanned items. Useful to retry "
"after fixing API key issues."
msgstr ""
"Reencola solo los adjuntos que fallaron con un error en la última ejecución "
"del escaneo externo. No afecta a los elementos ya escaneados. Útil para "
"reintentar después de corregir problemas con las claves de API."
#: includes/Admin/ToolsPage.php:640
msgid "Reset usage data"
msgstr "Restablecer datos de uso"
#: includes/Admin/ToolsPage.php:641
msgid ""
"Clears all usage rows and resets the usage-scan flag for every indexed "
"attachment. File metadata in the index is preserved. A background usage scan "
"is scheduled immediately after."
msgstr ""
"Elimina todas las filas de uso y restablece el indicador de escaneo de uso "
"para cada adjunto indexado. Los metadatos del archivo en el índice se "
"conservan. Se programa un escaneo en segundo plano inmediatamente después."
#: includes/Admin/ToolsPage.php:643
msgid "This will delete all usage data. Continue?"
msgstr "Esto eliminará todos los datos de uso. ¿Continuar?"
#: includes/Admin/ToolsPage.php:647
msgid "Re-index media library"
msgstr "Re-indexar la biblioteca de medios"
#: includes/Admin/ToolsPage.php:648
msgid ""
"Deletes all index and usage entries, then schedules a full fresh re-index. "
"External scan results are not affected. Useful if many media files have been "
"added or removed."
msgstr ""
"Elimina todas las entradas del índice y de uso, luego programa una re-"
"indexación completa. Los resultados del escaneo externo no se ven afectados. "
"Útil si se han añadido o eliminado muchos archivos multimedia."
#: includes/Admin/ToolsPage.php:650
msgid "This will delete all index and usage data. Continue?"
msgstr "Esto eliminará todos los datos del índice y de uso. ¿Continuar?"
#: includes/Admin/ToolsPage.php:654
msgid "Purge external data"
msgstr "Purgar datos externos"
#: includes/Admin/ToolsPage.php:655
msgid ""
"Deletes all external scan results and resets every attachment's external "
"status to pending. Use this before switching API providers or to force a "
"complete re-scan."
msgstr ""
"Elimina todos los resultados del escaneo externo y restablece el estado "
"externo de cada adjunto a pendiente. Úsalo antes de cambiar de proveedores "
"de API o para forzar un escaneo completo."
#: includes/Admin/ToolsPage.php:657
msgid "This will delete all external scan results. Continue?"
msgstr "Esto eliminará todos los resultados del escaneo externo. ¿Continuar?"
#: includes/Admin/ToolsPage.php:661
msgid "Reset all data"
msgstr "Restablecer todos los datos"
#: includes/Admin/ToolsPage.php:662
msgid ""
"Truncates all three plugin tables (index, usage, external results) and "
"schedules a fresh indexing run. Use this to start completely from scratch."
msgstr ""
"Vacía las tres tablas del plugin (índice, uso, resultados externos) y "
"programa una nueva indexación. Úsalo para empezar completamente desde cero."
#: includes/Admin/ToolsPage.php:664
msgid ""
"This will permanently delete ALL plugin data and cannot be undone. Are you "
"absolutely sure?"
msgstr ""
"Esto eliminará TODOS los datos del plugin de forma permanente y no se puede "
"deshacer. ¿Estás absolutamente seguro?"
#: includes/Admin/ToolsPage.php:675
msgid "Destructive operations"
msgstr "Operaciones destructivas"
#: includes/Admin/ToolsPage.php:678
msgid ""
"These operations permanently delete plugin data. Intended for forced "
"rescans, troubleshooting, or starting fresh. A confirmation prompt will "
"appear before any action is taken."
msgstr ""
"Estas operaciones eliminan datos del plugin de forma permanente. Pensadas "
"para forzar re-escaneos, solucionar problemas o empezar desde cero. "
"Aparecerá una confirmación antes de ejecutar cualquier acción."
#: includes/Admin/ToolsPage.php:726
msgid "Index media files"
msgstr "Indexar archivos multimedia"
#: includes/Admin/ToolsPage.php:727
msgid ""
"Discovers unindexed images and registers them in the plugin index. Run this "
"first."
msgstr ""
"Descubre imágenes no indexadas y las registra en el índice del plugin. "
"Ejecútalo primero."
#: includes/Admin/ToolsPage.php:731
msgid "Scan usage"
msgstr "Escanear uso"
#: includes/Admin/ToolsPage.php:732
msgid ""
"Checks every indexed image for usage across posts, pages, and custom fields."
msgstr ""
"Comprueba el uso de cada imagen indexada en entradas, páginas y campos "
"personalizados."
#: includes/Admin/ToolsPage.php:737
msgid ""
"Sends images to external providers for reverse-image search. Requires API "
"keys in Settings."
msgstr ""
"Envía imágenes a proveedores externos para búsqueda inversa de imágenes. "
"Requiere claves de API en Ajustes."
#: includes/Admin/ToolsPage.php:754
msgid "Run now"
msgstr "Ejecutar ahora"
#: includes/Admin/ToolsPage.php:757
msgid "Stop"
msgstr "Detener"
#. translators: %s: DB version number #. translators: %s: DB version number
#: includes/CLI/Command.php:49 #: includes/CLI/Command.php:49
#, php-format #, php-format
@ -495,7 +933,11 @@ msgstr "Ejecutando escaneos externos…"
msgid "External scan complete. %d attachments processed." msgid "External scan complete. %d attachments processed."
msgstr "Escaneo externo completo. %d adjuntos procesados." msgstr "Escaneo externo completo. %d adjuntos procesados."
#: includes/Core/Plugin.php:112 includes/Core/Plugin.php:113 #: includes/Core/Plugin.php:122 includes/Core/Plugin.php:123
msgid "Tools"
msgstr "Herramientas"
#: includes/Core/Plugin.php:135 includes/Core/Plugin.php:136
msgid "Settings" msgid "Settings"
msgstr "Ajustes" msgstr "Ajustes"
@ -523,14 +965,15 @@ msgstr "Último escaneo (interno)"
msgid "Last Scanned (external)" msgid "Last Scanned (external)"
msgstr "Último escaneo (externo)" msgstr "Último escaneo (externo)"
#: includes/Privacy/DataExporter.php:303
msgid "Provider"
msgstr "Proveedor"
#: includes/Privacy/DataExporter.php:307
msgid "Match Count"
msgstr "Número de coincidencias"
#: includes/Privacy/DataExporter.php:311 #: includes/Privacy/DataExporter.php:311
msgid "Scanned At" msgid "Scanned At"
msgstr "Escaneado el" msgstr "Escaneado el"
#~ msgid "https://robotstxt.es/plugins/media-audit/"
#~ msgstr "https://robotstxt.es/plugins/media-audit/"
#~ msgid "Javier Casares"
#~ msgstr "Javier Casares"
#~ msgid "https://javiercasares.com"
#~ msgstr "https://javiercasares.com"

View file

@ -1,15 +1,15 @@
# Copyright (C) 2026 Javier Casares # Copyright (C) 2026 ROBOTSTXT
# This file is distributed under the GPL-3.0-or-later. # This file is distributed under the GPL-3.0-or-later.
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Media Audit (by ROBOTSTXT) 1.0.0\n" "Project-Id-Version: Media Audit (by ROBOTSTXT) 1.1.0\n"
"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-mediaaudit\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/robotstxt-mediaaudit\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2026-05-02T06:06:40+00:00\n" "POT-Creation-Date: 2026-05-02T07:36:54+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.12.0\n" "X-Generator: WP-CLI 2.12.0\n"
"X-Domain: robotstxt-mediaaudit\n" "X-Domain: robotstxt-mediaaudit\n"
@ -21,7 +21,7 @@ msgstr ""
#. Plugin URI of the plugin #. Plugin URI of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "https://robotstxt.es/plugins/media-audit/" msgid "https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit"
msgstr "" msgstr ""
#. Description of the plugin #. Description of the plugin
@ -31,215 +31,243 @@ msgstr ""
#. Author of the plugin #. Author of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "Javier Casares" msgid "ROBOTSTXT"
msgstr "" msgstr ""
#. Author URI of the plugin #. Author URI of the plugin
#: robotstxt-mediaaudit.php #: robotstxt-mediaaudit.php
msgid "https://javiercasares.com" msgid "https://www.robotstxt.es/"
msgstr ""
#: includes/Admin/AuditPage.php:112
msgid "Loading…"
msgstr ""
#: includes/Admin/AuditPage.php:113
msgid "Error loading details."
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:114 #: includes/Admin/AuditPage.php:114
#: includes/Admin/AuditPage.php:190 msgid "Loading…"
msgid "This image is not used in any post."
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:115 #: includes/Admin/AuditPage.php:115
#: includes/Admin/AuditPage.php:152 msgid "Error loading details."
msgstr ""
#: includes/Admin/AuditPage.php:116
#: includes/Admin/AuditPage.php:192
msgid "This image is not used in any post."
msgstr ""
#: includes/Admin/AuditPage.php:117
#: includes/Admin/AuditPage.php:154
msgid "Usage Details" msgid "Usage Details"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:131 #: includes/Admin/AuditPage.php:133
#: includes/Admin/Settings.php:325 #: includes/Admin/Settings.php:325
#: includes/Admin/ToolsPage.php:245
msgid "You do not have permission to access this page." msgid "You do not have permission to access this page."
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:138 #: includes/Admin/AuditPage.php:140
#: includes/Core/Plugin.php:89 #: includes/Core/Plugin.php:99
#: includes/Core/Plugin.php:90 #: includes/Core/Plugin.php:100
#: includes/Core/Plugin.php:103 #: includes/Core/Plugin.php:113
#: includes/Core/Plugin.php:104 #: includes/Core/Plugin.php:114
#: includes/Privacy/DataEraser.php:33 #: includes/Privacy/DataEraser.php:33
#: includes/Privacy/DataExporter.php:45 #: includes/Privacy/DataExporter.php:45
msgid "Media Audit" msgid "Media Audit"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:144 #: includes/Admin/AuditPage.php:146
msgid "Search" msgid "Search"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:153 #: includes/Admin/AuditPage.php:155
msgid "Close" msgid "Close"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:171 #: includes/Admin/AuditPage.php:173
#: includes/Admin/AuditPage.php:459 #: includes/Admin/AuditPage.php:461
#: includes/Admin/AuditPage.php:510 #: includes/Admin/AuditPage.php:515
#: includes/Admin/AuditPage.php:542 #: includes/Admin/AuditPage.php:648
#: includes/Admin/AuditPage.php:680
#: includes/Admin/ToolsPage.php:105
#: includes/Admin/ToolsPage.php:161
msgid "Insufficient permissions." msgid "Insufficient permissions."
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:176 #: includes/Admin/AuditPage.php:178
msgid "Invalid attachment ID." msgid "Invalid attachment ID."
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:184 #: includes/Admin/AuditPage.php:186
msgid "Featured Image" msgid "Featured Image"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:185 #: includes/Admin/AuditPage.php:187
msgid "Post Content" msgid "Post Content"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:186 #: includes/Admin/AuditPage.php:188
msgid "Custom Field" msgid "Custom Field"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:194 #: includes/Admin/AuditPage.php:196
msgid "Post" msgid "Post"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:195 #: includes/Admin/AuditPage.php:197
msgid "Type" msgid "Type"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:196 #: includes/Admin/AuditPage.php:198
msgid "Context" msgid "Context"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:197 #: includes/Admin/AuditPage.php:199
msgid "Status" msgid "Status"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:240 #: includes/Admin/AuditPage.php:242
msgid "Internal Usage" msgid "Internal Usage"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:288 #: includes/Admin/AuditPage.php:290
msgid "External Scan Results" msgid "External Scan Results"
msgstr "" msgstr ""
#. translators: %d: number of external matches #. translators: %d: number of external matches
#: includes/Admin/AuditPage.php:312 #: includes/Admin/AuditPage.php:314
#, php-format #, php-format
msgid "%d match" msgid "%d match"
msgid_plural "%d matches" msgid_plural "%d matches"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: includes/Admin/AuditPage.php:317 #: includes/Admin/AuditPage.php:319
msgid "No matches" msgid "No matches"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:328 #: includes/Admin/AuditPage.php:330
msgid "Domain" msgid "Domain"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:329 #: includes/Admin/AuditPage.php:331
msgid "Occurrences" msgid "Occurrences"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:350 #: includes/Admin/AuditPage.php:352
msgid "Consensus Domains" msgid "Consensus Domains"
msgstr "" msgstr ""
#. translators: %d: number of providers that agree #. translators: %d: number of providers that agree
#: includes/Admin/AuditPage.php:356 #: includes/Admin/AuditPage.php:358
#, php-format #, php-format
msgid "%d provider" msgid "%d provider"
msgid_plural "%d providers" msgid_plural "%d providers"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
#: includes/Admin/AuditPage.php:388 #: includes/Admin/AuditPage.php:390
msgid "In Index" msgid "In Index"
msgstr "" msgstr ""
#. translators: %d: count of images not yet indexed #. translators: %d: count of images not yet indexed
#: includes/Admin/AuditPage.php:392 #: includes/Admin/AuditPage.php:394
#, php-format #, php-format
msgid "%d not yet indexed" msgid "%d not yet indexed"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:395 #: includes/Admin/AuditPage.php:397
msgid "All indexed" msgid "All indexed"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:400 #: includes/Admin/AuditPage.php:402
#: includes/Admin/MediaListTable.php:275 #: includes/Admin/MediaListTable.php:278
msgid "Scanned" msgid "Scanned"
msgstr "" msgstr ""
#. translators: %d: percentage #. translators: %d: percentage
#: includes/Admin/AuditPage.php:403 #: includes/Admin/AuditPage.php:405
#, php-format #, php-format
msgid "%d%% of index" msgid "%d%% of index"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:410 #: includes/Admin/AuditPage.php:412
msgid "Used" msgid "Used"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:416 #: includes/Admin/AuditPage.php:418
#: includes/Admin/MediaListTable.php:200 #: includes/Admin/MediaListTable.php:203
msgid "Unused" msgid "Unused"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:423 #: includes/Admin/AuditPage.php:425
msgid "External Matches" msgid "External Matches"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:473 #: includes/Admin/AuditPage.php:475
#: includes/Admin/AuditPage.php:529
msgid "Could not open output stream." msgid "Could not open output stream."
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:482 #: includes/Admin/AuditPage.php:484
#: includes/Admin/AuditPage.php:537
#: includes/Privacy/DataExporter.php:257 #: includes/Privacy/DataExporter.php:257
#: includes/Privacy/DataExporter.php:299 #: includes/Privacy/DataExporter.php:299
msgid "Attachment ID" msgid "Attachment ID"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:483 #: includes/Admin/AuditPage.php:485
#: includes/Admin/AuditPage.php:538
#: includes/Admin/MediaListTable.php:52 #: includes/Admin/MediaListTable.php:52
#: includes/Privacy/DataExporter.php:261 #: includes/Privacy/DataExporter.php:261
msgid "Filename" msgid "Filename"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:484 #: includes/Admin/AuditPage.php:486
#: includes/Admin/AuditPage.php:539
#: includes/Privacy/DataExporter.php:265 #: includes/Privacy/DataExporter.php:265
msgid "File URL" msgid "File URL"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:485 #: includes/Admin/AuditPage.php:487
msgid "MIME Type" msgid "MIME Type"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:486 #: includes/Admin/AuditPage.php:488
msgid "File Size (bytes)" msgid "File Size (bytes)"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:487 #: includes/Admin/AuditPage.php:489
#: includes/Admin/AuditPage.php:540
#: includes/Admin/MediaListTable.php:56 #: includes/Admin/MediaListTable.php:56
msgid "External Status" msgid "External Status"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:488 #: includes/Admin/AuditPage.php:490
#: includes/Admin/MediaListTable.php:54 #: includes/Admin/MediaListTable.php:54
msgid "Usage Count" msgid "Usage Count"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:489 #: includes/Admin/AuditPage.php:491
msgid "Used In (post titles)" msgid "Used In (post titles)"
msgstr "" msgstr ""
#: includes/Admin/AuditPage.php:541
#: includes/Admin/MediaListTable.php:57
msgid "Last Scanned"
msgstr ""
#: includes/Admin/AuditPage.php:542
#: includes/Privacy/DataExporter.php:303
msgid "Provider"
msgstr ""
#: includes/Admin/AuditPage.php:543
#: includes/Privacy/DataExporter.php:307
msgid "Match Count"
msgstr ""
#: includes/Admin/AuditPage.php:544
msgid "Top Domains"
msgstr ""
#: includes/Admin/MediaListTable.php:51 #: includes/Admin/MediaListTable.php:51
msgid "Thumbnail" msgid "Thumbnail"
msgstr "" msgstr ""
@ -252,73 +280,86 @@ msgstr ""
msgid "Usages" msgid "Usages"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:79 #: includes/Admin/MediaListTable.php:81
msgid "Run External Scan" msgid "Run External Scan"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:80 #: includes/Admin/MediaListTable.php:82
msgid "Purge External Data" msgid "Purge External Data"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:81 #: includes/Admin/MediaListTable.php:83
msgid "Export CSV" msgid "Export CSV"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:165 #: includes/Admin/MediaListTable.php:84
msgid "Export External Results CSV"
msgstr ""
#: includes/Admin/MediaListTable.php:168
msgid "Edit" msgid "Edit"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:170 #: includes/Admin/MediaListTable.php:173
msgid "View Details" msgid "View Details"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:236 #: includes/Admin/MediaListTable.php:239
msgid "(no title)" msgid "(no title)"
msgstr "" msgstr ""
#. translators: %d: number of additional usages #. translators: %d: number of additional usages
#: includes/Admin/MediaListTable.php:251 #: includes/Admin/MediaListTable.php:254
#, php-format #, php-format
msgid "+ %d more…" msgid "+ %d more…"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:273 #: includes/Admin/MediaListTable.php:276
#: includes/Admin/ToolsPage.php:580
#: includes/Admin/ToolsPage.php:581
#: includes/Admin/ToolsPage.php:582
msgid "Pending" msgid "Pending"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:274 #: includes/Admin/MediaListTable.php:277
msgid "Queued" msgid "Queued"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:276 #: includes/Admin/MediaListTable.php:279
msgid "Matches Found" msgid "Matches Found"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:277 #: includes/Admin/MediaListTable.php:280
msgid "Error" msgid "Error"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:305 #: includes/Admin/MediaListTable.php:308
msgid "View Results" msgid "View Results"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:336 #. translators: %s: human-readable time difference, e.g. "3 days"
#: includes/Admin/MediaListTable.php:350
#, php-format
msgid "%s ago"
msgstr ""
#: includes/Admin/MediaListTable.php:380
msgid "All statuses" msgid "All statuses"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:350 #: includes/Admin/MediaListTable.php:394
msgid "All post types" msgid "All post types"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:365 #: includes/Admin/MediaListTable.php:409
msgid "Unused only" msgid "Unused only"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:368 #: includes/Admin/MediaListTable.php:412
msgid "Filter" msgid "Filter"
msgstr "" msgstr ""
#: includes/Admin/MediaListTable.php:378 #: includes/Admin/MediaListTable.php:422
msgid "No indexed attachments found. Run wp mra scan-internal to populate the index." msgid "No indexed attachments found. Run wp mra scan-internal to populate the index."
msgstr "" msgstr ""
@ -423,6 +464,325 @@ msgstr ""
msgid "Maximum API requests per minute per provider (160). Default: 10." msgid "Maximum API requests per minute per provider (160). Default: 10."
msgstr "" msgstr ""
#: includes/Admin/ToolsPage.php:85
msgid "Done."
msgstr ""
#: includes/Admin/ToolsPage.php:86
msgid "Stopped."
msgstr ""
#: includes/Admin/ToolsPage.php:87
msgid "An error occurred."
msgstr ""
#: includes/Admin/ToolsPage.php:139
msgid "Invalid batch type."
msgstr ""
#: includes/Admin/ToolsPage.php:256
msgid "System Status"
msgstr ""
#: includes/Admin/ToolsPage.php:259
msgid "Operations"
msgstr ""
#: includes/Admin/ToolsPage.php:262
msgid "Direct scan runner"
msgstr ""
#: includes/Admin/ToolsPage.php:264
msgid "Run scan batches directly from your browser without relying on background jobs. Useful when WP-Cron is disabled or Action Scheduler is not running. Keep this tab open while processing."
msgstr ""
#: includes/Admin/ToolsPage.php:300
msgid "not installed"
msgstr ""
#: includes/Admin/ToolsPage.php:358
msgid "Internal scan scheduled."
msgstr ""
#: includes/Admin/ToolsPage.php:359
msgid "External scan scheduled."
msgstr ""
#: includes/Admin/ToolsPage.php:360
msgid "Failed scans re-queued. Background scan scheduled."
msgstr ""
#: includes/Admin/ToolsPage.php:361
msgid "Usage data cleared. Background scan scheduled."
msgstr ""
#: includes/Admin/ToolsPage.php:362
msgid "Index cleared. Fresh indexing scheduled."
msgstr ""
#: includes/Admin/ToolsPage.php:363
msgid "External scan data purged."
msgstr ""
#: includes/Admin/ToolsPage.php:364
msgid "All data reset. Fresh indexing scheduled."
msgstr ""
#: includes/Admin/ToolsPage.php:367
msgid "Operation completed."
msgstr ""
#: includes/Admin/ToolsPage.php:411
msgid "Database schema"
msgstr ""
#. translators: %s: version number
#: includes/Admin/ToolsPage.php:417
#, php-format
msgid "v%s up to date"
msgstr ""
#. translators: 1: stored version, 2: expected version
#: includes/Admin/ToolsPage.php:423
#, php-format
msgid "Installed: %1$s — Required: %2$s"
msgstr ""
#: includes/Admin/ToolsPage.php:432
msgid "Deactivate and reactivate the plugin to apply pending migrations."
msgstr ""
#: includes/Admin/ToolsPage.php:439
msgid "Action Scheduler"
msgstr ""
#: includes/Admin/ToolsPage.php:442
msgid "Available"
msgstr ""
#: includes/Admin/ToolsPage.php:444
msgid "Not available"
msgstr ""
#: includes/Admin/ToolsPage.php:449
msgid "Install and activate the Action Scheduler plugin. Background scanning will not work without it."
msgstr ""
#: includes/Admin/ToolsPage.php:456
msgid "WP-Cron"
msgstr ""
#: includes/Admin/ToolsPage.php:459
msgid "Disabled (DISABLE_WP_CRON)"
msgstr ""
#: includes/Admin/ToolsPage.php:461
msgid "Enabled"
msgstr ""
#: includes/Admin/ToolsPage.php:466
msgid "Background jobs will not fire automatically. Trigger the cron externally or use WP-CLI:"
msgstr ""
#: includes/Admin/ToolsPage.php:474
msgid "Internal index"
msgstr ""
#. translators: 1: indexed count, 2: total images
#: includes/Admin/ToolsPage.php:479
#, php-format
msgid "%1$s / %2$s images indexed"
msgstr ""
#. translators: %s: formatted count
#: includes/Admin/ToolsPage.php:490
#, php-format
msgid "%s image not yet indexed."
msgid_plural "%s images not yet indexed."
msgstr[0] ""
msgstr[1] ""
#: includes/Admin/ToolsPage.php:494
msgid "Schedule an internal scan below."
msgstr ""
#: includes/Admin/ToolsPage.php:502
msgid "Usage scan"
msgstr ""
#. translators: 1: scanned count, 2: total indexed
#: includes/Admin/ToolsPage.php:507
#, php-format
msgid "%1$s / %2$s scanned for usage"
msgstr ""
#. translators: %s: formatted count
#: includes/Admin/ToolsPage.php:518
#, php-format
msgid "%s pending."
msgid_plural "%s pending."
msgstr[0] ""
msgstr[1] ""
#: includes/Admin/ToolsPage.php:539
#: includes/Admin/ToolsPage.php:736
msgid "External scan"
msgstr ""
#. translators: 1: scanned 2: matches 3: queued 4: pending 5: errors
#: includes/Admin/ToolsPage.php:544
#, php-format
msgid "%1$s scanned · %2$s matches · %3$s queued · %4$s pending · %5$s errors"
msgstr ""
#: includes/Admin/ToolsPage.php:555
msgid "Some scans failed. Check that API keys are configured correctly in Settings."
msgstr ""
#: includes/Admin/ToolsPage.php:557
msgid "Schedule an external scan below to process pending items."
msgstr ""
#: includes/Admin/ToolsPage.php:575
msgid "Scheduled jobs"
msgstr ""
#: includes/Admin/ToolsPage.php:578
msgid "Action Scheduler not available."
msgstr ""
#: includes/Admin/ToolsPage.php:580
msgid "Index:"
msgstr ""
#: includes/Admin/ToolsPage.php:581
msgid "Usage:"
msgstr ""
#: includes/Admin/ToolsPage.php:582
msgid "External:"
msgstr ""
#: includes/Admin/ToolsPage.php:587
msgid "There is work to do but no background job is scheduled. Use the operations below."
msgstr ""
#: includes/Admin/ToolsPage.php:589
msgid "WP-Cron is disabled. Jobs will not fire automatically."
msgstr ""
#: includes/Admin/ToolsPage.php:599
msgid "WP-CLI commands to process scans manually:"
msgstr ""
#: includes/Admin/ToolsPage.php:616
msgid "Schedule internal scan"
msgstr ""
#: includes/Admin/ToolsPage.php:617
msgid "Queue a background job to index all unindexed media files and scan their usage across posts, pages, and custom fields. Safe to run at any time — will not re-process completed work."
msgstr ""
#: includes/Admin/ToolsPage.php:623
msgid "Schedule external scan"
msgstr ""
#: includes/Admin/ToolsPage.php:624
msgid "Mark all pending attachments as queued and launch a background reverse-image-search via the configured providers. Requires API keys in Settings."
msgstr ""
#: includes/Admin/ToolsPage.php:630
msgid "Requeue scan errors"
msgstr ""
#: includes/Admin/ToolsPage.php:631
msgid "Re-queues only the attachments that failed with an error on the last external scan run. Does not affect already-scanned items. Useful to retry after fixing API key issues."
msgstr ""
#: includes/Admin/ToolsPage.php:640
msgid "Reset usage data"
msgstr ""
#: includes/Admin/ToolsPage.php:641
msgid "Clears all usage rows and resets the usage-scan flag for every indexed attachment. File metadata in the index is preserved. A background usage scan is scheduled immediately after."
msgstr ""
#: includes/Admin/ToolsPage.php:643
msgid "This will delete all usage data. Continue?"
msgstr ""
#: includes/Admin/ToolsPage.php:647
msgid "Re-index media library"
msgstr ""
#: includes/Admin/ToolsPage.php:648
msgid "Deletes all index and usage entries, then schedules a full fresh re-index. External scan results are not affected. Useful if many media files have been added or removed."
msgstr ""
#: includes/Admin/ToolsPage.php:650
msgid "This will delete all index and usage data. Continue?"
msgstr ""
#: includes/Admin/ToolsPage.php:654
msgid "Purge external data"
msgstr ""
#: includes/Admin/ToolsPage.php:655
msgid "Deletes all external scan results and resets every attachment's external status to pending. Use this before switching API providers or to force a complete re-scan."
msgstr ""
#: includes/Admin/ToolsPage.php:657
msgid "This will delete all external scan results. Continue?"
msgstr ""
#: includes/Admin/ToolsPage.php:661
msgid "Reset all data"
msgstr ""
#: includes/Admin/ToolsPage.php:662
msgid "Truncates all three plugin tables (index, usage, external results) and schedules a fresh indexing run. Use this to start completely from scratch."
msgstr ""
#: includes/Admin/ToolsPage.php:664
msgid "This will permanently delete ALL plugin data and cannot be undone. Are you absolutely sure?"
msgstr ""
#: includes/Admin/ToolsPage.php:675
msgid "Destructive operations"
msgstr ""
#: includes/Admin/ToolsPage.php:678
msgid "These operations permanently delete plugin data. Intended for forced rescans, troubleshooting, or starting fresh. A confirmation prompt will appear before any action is taken."
msgstr ""
#: includes/Admin/ToolsPage.php:726
msgid "Index media files"
msgstr ""
#: includes/Admin/ToolsPage.php:727
msgid "Discovers unindexed images and registers them in the plugin index. Run this first."
msgstr ""
#: includes/Admin/ToolsPage.php:731
msgid "Scan usage"
msgstr ""
#: includes/Admin/ToolsPage.php:732
msgid "Checks every indexed image for usage across posts, pages, and custom fields."
msgstr ""
#: includes/Admin/ToolsPage.php:737
msgid "Sends images to external providers for reverse-image search. Requires API keys in Settings."
msgstr ""
#: includes/Admin/ToolsPage.php:754
msgid "Run now"
msgstr ""
#: includes/Admin/ToolsPage.php:757
msgid "Stop"
msgstr ""
#. translators: %s: DB version number #. translators: %s: DB version number
#: includes/CLI/Command.php:49 #: includes/CLI/Command.php:49
#, php-format #, php-format
@ -484,8 +844,13 @@ msgstr ""
msgid "External scan complete. %d attachments processed." msgid "External scan complete. %d attachments processed."
msgstr "" msgstr ""
#: includes/Core/Plugin.php:112 #: includes/Core/Plugin.php:122
#: includes/Core/Plugin.php:113 #: includes/Core/Plugin.php:123
msgid "Tools"
msgstr ""
#: includes/Core/Plugin.php:135
#: includes/Core/Plugin.php:136
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
@ -513,14 +878,6 @@ msgstr ""
msgid "Last Scanned (external)" msgid "Last Scanned (external)"
msgstr "" msgstr ""
#: includes/Privacy/DataExporter.php:303
msgid "Provider"
msgstr ""
#: includes/Privacy/DataExporter.php:307
msgid "Match Count"
msgstr ""
#: includes/Privacy/DataExporter.php:311 #: includes/Privacy/DataExporter.php:311
msgid "Scanned At" msgid "Scanned At"
msgstr "" msgstr ""

Binary file not shown.

View file

@ -1,11 +1,11 @@
=== Media Audit (by ROBOTSTXT) === === Media Audit (by ROBOTSTXT) ===
Contributors: javiercasares Contributors: javiercasares, robotstxt
Tags: media, copyright, images, reverse image search, media library Tags: media, copyright, images, reverse image search, media library
Requires at least: 6.8 Requires at least: 6.8
Tested up to: 7.0 Tested up to: 7.0
Requires PHP: 8.2 Requires PHP: 8.2
Requires Plugins: action-scheduler Requires Plugins: action-scheduler
Stable tag: 1.0.0 Stable tag: 1.2.0
License: GPL-3.0-or-later License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.txt License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@ -98,6 +98,17 @@ By default, no. Enable **Settings → Delete data on uninstall** if you want all
Only the 3 latest versions. The full changelog is in [changelog.txt](changelog.txt). Only the 3 latest versions. The full changelog is in [changelog.txt](changelog.txt).
= 1.2.0 =
* Added browser-based AJAX scan runner in the Tools page (runs without WP-Cron or Action Scheduler).
* Added "Requeue scan errors" operation to retry failed external scans after fixing API key issues.
* Added "Last Scanned" column in the audit list, sortable with NULLs-last ordering.
* Added bulk action to export external scan results as a CSV file.
= 1.1.0 =
* Updated plugin URI, author, and contributor metadata.
= 1.0.0 = = 1.0.0 =
* First stable release. Internal usage scanning, external reverse image search (Google Vision + TinEye), consensus detection, WP-CLI commands, GDPR privacy tools, and Spanish/Catalan translations. * First stable release. Internal usage scanning, external reverse image search (Google Vision + TinEye), consensus detection, WP-CLI commands, GDPR privacy tools, and Spanish/Catalan translations.

View file

@ -1,15 +1,16 @@
<?php <?php
/** /**
* Plugin Name: Media Audit (by ROBOTSTXT) * Plugin Name: Media Audit (by ROBOTSTXT)
* Plugin URI: https://robotstxt.es/plugins/media-audit/ * Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-mediaaudit
* Description: Internal media library usage auditing and external reverse image search to detect potential copyright issues. * Description: Internal media library usage auditing and external reverse image search to detect potential copyright issues.
* Version: 1.0.0 * Version: 1.2.0
* Requires at least: 6.8 * Requires at least: 6.8
* Tested up to: 7.0 * Tested up to: 7.0
* Requires PHP: 8.2 * Requires PHP: 8.2
* Requires Plugins: action-scheduler * Requires Plugins: action-scheduler
* Author: Javier Casares * Author: ROBOTSTXT
* Author URI: https://javiercasares.com * Author URI: https://www.robotstxt.es/
* Contributors: javiercasares, robotstxt
* License: GPL-3.0-or-later * License: GPL-3.0-or-later
* License URI: https://www.gnu.org/licenses/gpl-3.0.txt * License URI: https://www.gnu.org/licenses/gpl-3.0.txt
* Text Domain: robotstxt-mediaaudit * Text Domain: robotstxt-mediaaudit
@ -22,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) {
exit; exit;
} }
define( 'ROBOTSTXT_MEDIAAUDIT_VERSION', '1.0.0' ); define( 'ROBOTSTXT_MEDIAAUDIT_VERSION', '1.2.0' );
define( 'ROBOTSTXT_MEDIAAUDIT_DB_VERSION', '1.0.1' ); define( 'ROBOTSTXT_MEDIAAUDIT_DB_VERSION', '1.0.1' );
define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_FILE', __FILE__ ); define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_FILE', __FILE__ );
define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'ROBOTSTXT_MEDIAAUDIT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );

View file

@ -10,6 +10,7 @@ return array(
'MediaRightsAudit\\Admin\\AuditPage' => $baseDir . '/includes/Admin/AuditPage.php', 'MediaRightsAudit\\Admin\\AuditPage' => $baseDir . '/includes/Admin/AuditPage.php',
'MediaRightsAudit\\Admin\\MediaListTable' => $baseDir . '/includes/Admin/MediaListTable.php', 'MediaRightsAudit\\Admin\\MediaListTable' => $baseDir . '/includes/Admin/MediaListTable.php',
'MediaRightsAudit\\Admin\\Settings' => $baseDir . '/includes/Admin/Settings.php', 'MediaRightsAudit\\Admin\\Settings' => $baseDir . '/includes/Admin/Settings.php',
'MediaRightsAudit\\Admin\\ToolsPage' => $baseDir . '/includes/Admin/ToolsPage.php',
'MediaRightsAudit\\CLI\\Command' => $baseDir . '/includes/CLI/Command.php', 'MediaRightsAudit\\CLI\\Command' => $baseDir . '/includes/CLI/Command.php',
'MediaRightsAudit\\Core\\Activator' => $baseDir . '/includes/Core/Activator.php', 'MediaRightsAudit\\Core\\Activator' => $baseDir . '/includes/Core/Activator.php',
'MediaRightsAudit\\Core\\Database' => $baseDir . '/includes/Core/Database.php', 'MediaRightsAudit\\Core\\Database' => $baseDir . '/includes/Core/Database.php',

View file

@ -25,6 +25,7 @@ class ComposerStaticInit957728ab3efa005f456e5f9df13a19d2
'MediaRightsAudit\\Admin\\AuditPage' => __DIR__ . '/../..' . '/includes/Admin/AuditPage.php', 'MediaRightsAudit\\Admin\\AuditPage' => __DIR__ . '/../..' . '/includes/Admin/AuditPage.php',
'MediaRightsAudit\\Admin\\MediaListTable' => __DIR__ . '/../..' . '/includes/Admin/MediaListTable.php', 'MediaRightsAudit\\Admin\\MediaListTable' => __DIR__ . '/../..' . '/includes/Admin/MediaListTable.php',
'MediaRightsAudit\\Admin\\Settings' => __DIR__ . '/../..' . '/includes/Admin/Settings.php', 'MediaRightsAudit\\Admin\\Settings' => __DIR__ . '/../..' . '/includes/Admin/Settings.php',
'MediaRightsAudit\\Admin\\ToolsPage' => __DIR__ . '/../..' . '/includes/Admin/ToolsPage.php',
'MediaRightsAudit\\CLI\\Command' => __DIR__ . '/../..' . '/includes/CLI/Command.php', 'MediaRightsAudit\\CLI\\Command' => __DIR__ . '/../..' . '/includes/CLI/Command.php',
'MediaRightsAudit\\Core\\Activator' => __DIR__ . '/../..' . '/includes/Core/Activator.php', 'MediaRightsAudit\\Core\\Activator' => __DIR__ . '/../..' . '/includes/Core/Activator.php',
'MediaRightsAudit\\Core\\Database' => __DIR__ . '/../..' . '/includes/Core/Database.php', 'MediaRightsAudit\\Core\\Database' => __DIR__ . '/../..' . '/includes/Core/Database.php',