diff --git a/CHANGELOG.md b/CHANGELOG.md
index be25231..63dacc5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.3.0] - 2026-01-29
+
+### Added
+
+- License management integration using `magdev/wc-licensed-product-client` package
+- Tabbed settings page with License, Default Settings, and Integrations tabs
+- License validation and activation via AJAX with real-time status updates
+- License status banner showing current license state and expiration
+- License checks for frontend components (unlicensed sites show message instead of content)
+
+### Changed
+
+- Reorganized settings page into three tabs for better organization
+- Frontend features (player, shortcodes, ActivityPub) now require valid license
+- Admin/backend functionality works regardless of license status
+
+### Security
+
+- Server secret stored securely in WordPress options
+- HMAC signature verification for license server responses
+- Nonce verification for all license AJAX operations
+
## [0.2.0] - 2026-01-28
### Added
diff --git a/CLAUDE.md b/CLAUDE.md
index d443e4e..b0f3c4e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -24,9 +24,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
-### Version 0.2.1 (Bugfix)
-
-### Version 0.3.0 (Minor)
+(No pending features - all roadmap items completed)
## Technical Stack
@@ -426,3 +424,43 @@ wp-fedistream/
- All releases pushed to origin (v0.1.1 and v0.2.0 tags)
- Markdown linting fixes applied to USERGUIDE.md
+
+### 2026-01-29 - License Management v0.3.0
+
+**Summary:** Implemented license management integration and reorganized settings page into tabs.
+
+**Features:**
+
+- License management using `magdev/wc-licensed-product-client` package
+- Tabbed settings page: License, Default Settings, Integrations
+- License validation and activation via AJAX
+- License status banner with expiration display
+- Frontend license checks (unlicensed sites show message instead of content)
+- Admin/backend works regardless of license status
+
+**License Behavior:**
+
+- Backend (admin): Full access always
+- Frontend (player, shortcodes, ActivityPub): Requires valid license
+
+**Files Created:**
+
+- `includes/License/Manager.php` - License management wrapper class
+
+**Files Modified:**
+
+- `composer.json` - Added VCS repository and `magdev/wc-licensed-product-client` dependency
+- `includes/Plugin.php` - Tabbed settings page, license manager initialization, conditional frontend loading
+- `includes/Installer.php` - Added default license options
+- `includes/Frontend/Shortcodes.php` - Added unlicensed mode support
+- `includes/Frontend/Ajax.php` - Added license checks to public AJAX endpoints
+- `assets/js/admin.js` - License validation AJAX handlers
+- `assets/css/admin.css` - Tab and license status styling
+- `wp-fedistream.php` - Version bump to 0.3.0
+- `CHANGELOG.md` - Added v0.3.0 entry
+
+**Notes:**
+
+- Package name is `magdev/wc-licensed-product-client` (not `wc-license-product-client`)
+- Uses Symfony HTTP Client via the license client package
+- License validation cached for 24 hours using WordPress transients
diff --git a/assets/css/admin.css b/assets/css/admin.css
index b0413b9..f317e93 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -4,4 +4,122 @@
* @package WP_FediStream
*/
-/* Admin styles will be added here */
+/* Settings page tabs */
+.nav-tab-wrapper + .fedistream-settings-content {
+ margin-top: -1px;
+}
+
+.fedistream-settings-content {
+ background: #fff;
+ border: 1px solid #c3c4c7;
+ border-top: none;
+ padding: 20px;
+}
+
+/* Active tab styling */
+.wrap .nav-tab-wrapper .nav-tab-active {
+ background: #fff;
+ border-bottom-color: #fff;
+}
+
+/* License status banner */
+.fedistream-license-status {
+ margin-bottom: 20px;
+}
+
+.fedistream-license-status .dashicons {
+ color: inherit;
+}
+
+.fedistream-license-status.notice-success .dashicons {
+ color: #00a32a;
+}
+
+.fedistream-license-status.notice-error .dashicons {
+ color: #d63638;
+}
+
+.fedistream-license-status.notice-warning .dashicons {
+ color: #dba617;
+}
+
+.fedistream-license-status.notice-info .dashicons {
+ color: #72aee6;
+}
+
+/* License form buttons */
+#fedistream-license-form .button .dashicons {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ line-height: 1.3;
+}
+
+/* License message display */
+#fedistream-license-message {
+ padding: 10px 15px;
+}
+
+#fedistream-license-message p {
+ margin: 0;
+}
+
+/* Dashboard stats grid */
+.fedistream-stats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 20px;
+ margin: 20px 0;
+}
+
+.fedistream-stat-box {
+ background: #fff;
+ padding: 20px;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+}
+
+.fedistream-stat-box h3 {
+ margin: 0 0 10px;
+}
+
+.fedistream-stat-box p {
+ font-size: 2em;
+ margin: 0;
+ color: #2271b1;
+}
+
+/* Quick actions */
+.fedistream-quick-actions {
+ background: #fff;
+ padding: 20px;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+ margin: 20px 0;
+}
+
+/* Info box */
+.fedistream-info {
+ background: #fff;
+ padding: 20px;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+}
+
+/* Responsive adjustments */
+@media screen and (max-width: 782px) {
+ .fedistream-stats {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ #fedistream-license-form .button {
+ display: block;
+ margin: 10px 0 0 0 !important;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ .fedistream-stats {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/assets/js/admin.js b/assets/js/admin.js
index c79908e..fb6aa33 100644
--- a/assets/js/admin.js
+++ b/assets/js/admin.js
@@ -8,7 +8,115 @@
'use strict';
$(document).ready(function() {
- // Admin scripts will be added here
+ // License validation functionality
+ initLicenseValidation();
});
+ /**
+ * Initialize license validation AJAX handlers.
+ */
+ function initLicenseValidation() {
+ var $validateBtn = $('#fedistream-validate-license');
+ var $activateBtn = $('#fedistream-activate-license');
+ var $spinner = $('#fedistream-license-spinner');
+ var $message = $('#fedistream-license-message');
+
+ if (!$validateBtn.length) {
+ return;
+ }
+
+ // Validate license button
+ $validateBtn.on('click', function(e) {
+ e.preventDefault();
+ performLicenseAction('fedistream_validate_license', 'Validating...');
+ });
+
+ // Activate license button
+ $activateBtn.on('click', function(e) {
+ e.preventDefault();
+ performLicenseAction('fedistream_activate_license', 'Activating...');
+ });
+
+ /**
+ * Perform license AJAX action.
+ *
+ * @param {string} action AJAX action name.
+ * @param {string} loadingText Loading button text.
+ */
+ function performLicenseAction(action, loadingText) {
+ var originalText = $validateBtn.text();
+
+ // Show loading state
+ $spinner.addClass('is-active');
+ $validateBtn.prop('disabled', true);
+ $activateBtn.prop('disabled', true);
+ $message.hide();
+
+ $.ajax({
+ url: ajaxurl,
+ type: 'POST',
+ data: {
+ action: action,
+ nonce: fedistreamLicenseNonce
+ },
+ success: function(response) {
+ $spinner.removeClass('is-active');
+ $validateBtn.prop('disabled', false);
+ $activateBtn.prop('disabled', false);
+
+ if (response.success) {
+ showMessage('success', response.data.message);
+ // Reload page to show updated status
+ setTimeout(function() {
+ window.location.reload();
+ }, 1500);
+ } else {
+ showMessage('error', response.data.message || 'An error occurred.');
+ }
+ },
+ error: function(xhr, status, error) {
+ $spinner.removeClass('is-active');
+ $validateBtn.prop('disabled', false);
+ $activateBtn.prop('disabled', false);
+ showMessage('error', 'Request failed. Please try again.');
+ }
+ });
+ }
+
+ /**
+ * Show a message to the user.
+ *
+ * @param {string} type Message type: 'success', 'error', 'warning', 'info'.
+ * @param {string} text Message text.
+ */
+ function showMessage(type, text) {
+ var classMap = {
+ 'success': 'notice-success',
+ 'error': 'notice-error',
+ 'warning': 'notice-warning',
+ 'info': 'notice-info'
+ };
+
+ var noticeClass = classMap[type] || 'notice-info';
+
+ $message
+ .removeClass('notice-success notice-error notice-warning notice-info')
+ .addClass('notice ' + noticeClass)
+ .html('
' + escapeHtml(text) + '
')
+ .show();
+ }
+
+ /**
+ * Escape HTML entities.
+ *
+ * @param {string} text Text to escape.
+ * @return {string} Escaped text.
+ */
+ function escapeHtml(text) {
+ var div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+ }
+
})(jQuery);
diff --git a/composer.json b/composer.json
index bc80e1a..3640392 100644
--- a/composer.json
+++ b/composer.json
@@ -14,8 +14,15 @@
"support": {
"issues": "https://src.bundespruefstelle.ch/magdev/wp-fedistream/issues"
},
+ "repositories": [
+ {
+ "type": "vcs",
+ "url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git"
+ }
+ ],
"require": {
"php": ">=8.3",
+ "magdev/wc-licensed-product-client": "^0.1",
"twig/twig": "^3.0"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index d979ca9..5ac7bd5 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,312 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "c8fb50541e5730c8ad92b76392765aca",
+ "content-hash": "29e8e4e069b25dee0a610019a77dab50",
"packages": [
+ {
+ "name": "magdev/wc-licensed-product-client",
+ "version": "v0.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client.git",
+ "reference": "83037ea0c2d9e365cf9ec0ad50251d3ebc7e4782"
+ },
+ "require": {
+ "php": "^8.3",
+ "psr/cache": "^3.0",
+ "psr/http-client": "^1.0",
+ "psr/log": "^3.0",
+ "symfony/http-client": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^11.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Magdev\\WcLicensedProductClient\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Magdev\\WcLicensedProductClient\\Tests\\": "tests/"
+ }
+ },
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Marco Graetsch",
+ "email": "magdev3.0@gmail.com",
+ "homepage": "https://src.bundespruefstelle.ch/magdev"
+ }
+ ],
+ "description": "Client library for WooCommerce Licensed Product Plugin - Activate, validate and check the status of licenses via REST API",
+ "homepage": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client",
+ "support": {
+ "issues": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client/issues",
+ "source": "https://src.bundespruefstelle.ch/magdev/wc-licensed-product-client"
+ },
+ "time": "2026-01-22T15:24:57+00:00"
+ },
+ {
+ "name": "psr/cache",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/3.0.0"
+ },
+ "time": "2021-02-03T23:26:27+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
{
"name": "symfony/deprecation-contracts",
"version": "v3.6.0",
@@ -73,6 +377,185 @@
],
"time": "2024-09-25T14:21:43+00:00"
},
+ {
+ "name": "symfony/http-client",
+ "version": "v7.4.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client.git",
+ "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f",
+ "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.2",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-client-contracts": "~3.4.4|^3.5.2",
+ "symfony/polyfill-php83": "^1.29",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "amphp/amp": "<2.5",
+ "amphp/socket": "<1.1",
+ "php-http/discovery": "<1.15",
+ "symfony/http-foundation": "<6.4"
+ },
+ "provide": {
+ "php-http/async-client-implementation": "*",
+ "php-http/client-implementation": "*",
+ "psr/http-client-implementation": "1.0",
+ "symfony/http-client-implementation": "3.0"
+ },
+ "require-dev": {
+ "amphp/http-client": "^4.2.1|^5.0",
+ "amphp/http-tunnel": "^1.0|^2.0",
+ "guzzlehttp/promises": "^1.4|^2.0",
+ "nyholm/psr7": "^1.0",
+ "php-http/httplug": "^1.0|^2.0",
+ "psr/http-client": "^1.0",
+ "symfony/amphp-http-client-meta": "^1.0|^2.0",
+ "symfony/cache": "^6.4|^7.0|^8.0",
+ "symfony/dependency-injection": "^6.4|^7.0|^8.0",
+ "symfony/http-kernel": "^6.4|^7.0|^8.0",
+ "symfony/messenger": "^6.4|^7.0|^8.0",
+ "symfony/process": "^6.4|^7.0|^8.0",
+ "symfony/rate-limiter": "^6.4|^7.0|^8.0",
+ "symfony/stopwatch": "^6.4|^7.0|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "http"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client/tree/v7.4.5"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-27T16:16:02+00:00"
+ },
+ {
+ "name": "symfony/http-client-contracts",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-client-contracts.git",
+ "reference": "75d7043853a42837e68111812f4d964b01e5101c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c",
+ "reference": "75d7043853a42837e68111812f4d964b01e5101c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\HttpClient\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to HTTP clients",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-04-29T11:18:49+00:00"
+ },
{
"name": "symfony/polyfill-ctype",
"version": "v1.33.0",
@@ -241,6 +724,173 @@
],
"time": "2024-12-23T08:48:59+00:00"
},
+ {
+ "name": "symfony/polyfill-php83",
+ "version": "v1.33.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php83.git",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php83\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-07-08T02:45:35+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.6.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.6.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-07-15T11:30:57+00:00"
+ },
{
"name": "twig/twig",
"version": "v3.23.0",
diff --git a/includes/Frontend/Ajax.php b/includes/Frontend/Ajax.php
index 0cd03e3..1f08ec8 100644
--- a/includes/Frontend/Ajax.php
+++ b/includes/Frontend/Ajax.php
@@ -7,6 +7,8 @@
namespace WP_FediStream\Frontend;
+use WP_FediStream\License\Manager as LicenseManager;
+
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
@@ -36,6 +38,11 @@ class Ajax {
* @return void
*/
public function get_track(): void {
+ // Check license.
+ if ( ! LicenseManager::is_license_valid() ) {
+ wp_send_json_error( array( 'message' => __( 'This feature requires a valid license.', 'wp-fedistream' ) ) );
+ }
+
// Verify nonce.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) );
@@ -125,6 +132,11 @@ class Ajax {
* @return void
*/
public function record_play(): void {
+ // Check license.
+ if ( ! LicenseManager::is_license_valid() ) {
+ wp_send_json_error( array( 'message' => __( 'This feature requires a valid license.', 'wp-fedistream' ) ) );
+ }
+
// Verify nonce.
if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp-fedistream-nonce' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'wp-fedistream' ) ) );
diff --git a/includes/Frontend/Shortcodes.php b/includes/Frontend/Shortcodes.php
index 12c0759..411456f 100644
--- a/includes/Frontend/Shortcodes.php
+++ b/includes/Frontend/Shortcodes.php
@@ -27,13 +27,35 @@ class Shortcodes {
private Plugin $plugin;
/**
- * Constructor.
+ * Whether running in unlicensed mode.
+ *
+ * @var bool
*/
- public function __construct() {
- $this->plugin = Plugin::get_instance();
+ private bool $unlicensed_mode = false;
+
+ /**
+ * Constructor.
+ *
+ * @param bool $unlicensed_mode Whether to run in unlicensed mode.
+ */
+ public function __construct( bool $unlicensed_mode = false ) {
+ $this->plugin = Plugin::get_instance();
+ $this->unlicensed_mode = $unlicensed_mode;
$this->register_shortcodes();
}
+ /**
+ * Get the unlicensed message HTML.
+ *
+ * @return string
+ */
+ private function get_unlicensed_message(): string {
+ return ''
+ . '
'
+ . esc_html__( 'This content requires a valid FediStream license.', 'wp-fedistream' )
+ . '
';
+ }
+
/**
* Register all shortcodes.
*
@@ -501,6 +523,11 @@ class Shortcodes {
* @return string
*/
private function render_template( string $template, array $context ): string {
+ // Check for unlicensed mode.
+ if ( $this->unlicensed_mode ) {
+ return $this->get_unlicensed_message();
+ }
+
try {
return $this->plugin->render( $template, $context );
} catch ( \Exception $e ) {
diff --git a/includes/Installer.php b/includes/Installer.php
index 9f9cd29..9be9892 100644
--- a/includes/Installer.php
+++ b/includes/Installer.php
@@ -345,11 +345,18 @@ class Installer {
*/
private static function set_default_options(): void {
$defaults = array(
- 'wp_fedistream_enable_activitypub' => 1,
- 'wp_fedistream_enable_woocommerce' => 0,
- 'wp_fedistream_audio_formats' => array( 'mp3', 'wav', 'flac', 'ogg' ),
- 'wp_fedistream_max_upload_size' => 50, // MB
- 'wp_fedistream_default_license' => 'all-rights-reserved',
+ 'wp_fedistream_enable_activitypub' => 1,
+ 'wp_fedistream_enable_woocommerce' => 0,
+ 'wp_fedistream_audio_formats' => array( 'mp3', 'wav', 'flac', 'ogg' ),
+ 'wp_fedistream_max_upload_size' => 50, // MB
+ 'wp_fedistream_default_license' => 'all-rights-reserved',
+ // License management options.
+ 'wp_fedistream_license_key' => '',
+ 'wp_fedistream_license_server_url' => '',
+ 'wp_fedistream_license_server_secret' => '',
+ 'wp_fedistream_license_status' => 'unchecked',
+ 'wp_fedistream_license_data' => array(),
+ 'wp_fedistream_license_last_check' => 0,
);
foreach ( $defaults as $option => $value ) {
diff --git a/includes/License/Manager.php b/includes/License/Manager.php
new file mode 100644
index 0000000..f388480
--- /dev/null
+++ b/includes/License/Manager.php
@@ -0,0 +1,653 @@
+init_hooks();
+ }
+
+ /**
+ * Initialize WordPress hooks.
+ *
+ * @return void
+ */
+ private function init_hooks(): void {
+ add_action( 'wp_ajax_fedistream_validate_license', array( $this, 'ajax_validate_license' ) );
+ add_action( 'wp_ajax_fedistream_activate_license', array( $this, 'ajax_activate_license' ) );
+ add_action( 'wp_ajax_fedistream_deactivate_license', array( $this, 'ajax_deactivate_license' ) );
+ add_action( 'wp_ajax_fedistream_check_license_status', array( $this, 'ajax_check_status' ) );
+ }
+
+ /**
+ * Initialize the license client.
+ *
+ * @return bool True if client was initialized successfully.
+ */
+ private function init_client(): bool {
+ if ( null !== $this->client ) {
+ return true;
+ }
+
+ $server_url = self::get_server_url();
+ $server_secret = self::get_server_secret();
+
+ if ( empty( $server_url ) || empty( $server_secret ) ) {
+ return false;
+ }
+
+ try {
+ $this->client = new SecureLicenseClient(
+ httpClient: HttpClient::create(),
+ baseUrl: $server_url,
+ serverSecret: $server_secret,
+ );
+ return true;
+ } catch ( \Throwable $e ) {
+ return false;
+ }
+ }
+
+ /**
+ * Validate the current license.
+ *
+ * @return array{success: bool, message: string, data?: array}
+ */
+ public function validate(): array {
+ if ( ! $this->init_client() ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'License server configuration is incomplete.', 'wp-fedistream' ),
+ );
+ }
+
+ $license_key = self::get_license_key();
+ if ( empty( $license_key ) ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'No license key provided.', 'wp-fedistream' ),
+ );
+ }
+
+ $domain = wp_parse_url( home_url(), PHP_URL_HOST );
+
+ try {
+ $result = $this->client->validate( $license_key, $domain );
+
+ // Update cached status.
+ $this->update_cached_status( 'valid', array(
+ 'product_id' => $result->productId,
+ 'expires_at' => $result->expiresAt?->format( 'c' ),
+ 'version_id' => $result->versionId,
+ ) );
+
+ return array(
+ 'success' => true,
+ 'message' => __( 'License validated successfully.', 'wp-fedistream' ),
+ 'data' => array(
+ 'status' => 'valid',
+ 'product_id' => $result->productId,
+ 'expires_at' => $result->expiresAt?->format( 'Y-m-d' ),
+ 'lifetime' => $result->isLifetime(),
+ ),
+ );
+ } catch ( LicenseNotFoundException $e ) {
+ $this->update_cached_status( 'invalid' );
+ return array(
+ 'success' => false,
+ 'message' => __( 'License key not found. Please check your license key.', 'wp-fedistream' ),
+ );
+ } catch ( LicenseExpiredException $e ) {
+ $this->update_cached_status( 'expired' );
+ return array(
+ 'success' => false,
+ 'message' => __( 'Your license has expired. Please renew to continue.', 'wp-fedistream' ),
+ );
+ } catch ( LicenseRevokedException $e ) {
+ $this->update_cached_status( 'revoked' );
+ return array(
+ 'success' => false,
+ 'message' => __( 'Your license has been revoked.', 'wp-fedistream' ),
+ );
+ } catch ( LicenseInactiveException $e ) {
+ $this->update_cached_status( 'inactive' );
+ return array(
+ 'success' => false,
+ 'message' => __( 'License is inactive. Please activate it first.', 'wp-fedistream' ),
+ );
+ } catch ( DomainMismatchException $e ) {
+ $this->update_cached_status( 'invalid' );
+ return array(
+ 'success' => false,
+ 'message' => __( 'This license is not activated for this domain.', 'wp-fedistream' ),
+ );
+ } catch ( SignatureException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'License verification failed. Please check your server secret.', 'wp-fedistream' ),
+ );
+ } catch ( RateLimitExceededException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'Too many requests. Please try again later.', 'wp-fedistream' ),
+ );
+ } catch ( LicenseException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => sprintf(
+ /* translators: %s: Error message */
+ __( 'License validation failed: %s', 'wp-fedistream' ),
+ $e->getMessage()
+ ),
+ );
+ } catch ( \Throwable $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'Unable to verify license. Please try again later.', 'wp-fedistream' ),
+ );
+ }
+ }
+
+ /**
+ * Activate the license for this domain.
+ *
+ * @return array{success: bool, message: string, data?: array}
+ */
+ public function activate(): array {
+ if ( ! $this->init_client() ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'License server configuration is incomplete.', 'wp-fedistream' ),
+ );
+ }
+
+ $license_key = self::get_license_key();
+ if ( empty( $license_key ) ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'No license key provided.', 'wp-fedistream' ),
+ );
+ }
+
+ $domain = wp_parse_url( home_url(), PHP_URL_HOST );
+
+ try {
+ $result = $this->client->activate( $license_key, $domain );
+
+ if ( $result->success ) {
+ // Validate after activation to get full license info.
+ return $this->validate();
+ }
+
+ return array(
+ 'success' => false,
+ 'message' => $result->message,
+ );
+ } catch ( MaxActivationsReachedException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'Maximum number of activations reached. Please deactivate another site first.', 'wp-fedistream' ),
+ );
+ } catch ( LicenseNotFoundException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'License key not found. Please check your license key.', 'wp-fedistream' ),
+ );
+ } catch ( LicenseExpiredException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'Your license has expired. Please renew to continue.', 'wp-fedistream' ),
+ );
+ } catch ( SignatureException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'License verification failed. Please check your server secret.', 'wp-fedistream' ),
+ );
+ } catch ( LicenseException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => sprintf(
+ /* translators: %s: Error message */
+ __( 'License activation failed: %s', 'wp-fedistream' ),
+ $e->getMessage()
+ ),
+ );
+ } catch ( \Throwable $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'Unable to activate license. Please try again later.', 'wp-fedistream' ),
+ );
+ }
+ }
+
+ /**
+ * Get the current license status.
+ *
+ * @param bool $force_refresh Force a fresh check from the server.
+ * @return array{success: bool, message: string, data?: array}
+ */
+ public function get_status( bool $force_refresh = false ): array {
+ // Check cached status first.
+ if ( ! $force_refresh ) {
+ $cached = $this->get_cached_validation();
+ if ( null !== $cached ) {
+ return $cached;
+ }
+ }
+
+ if ( ! $this->init_client() ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'License server configuration is incomplete.', 'wp-fedistream' ),
+ 'data' => array(
+ 'status' => 'unconfigured',
+ ),
+ );
+ }
+
+ $license_key = self::get_license_key();
+ if ( empty( $license_key ) ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'No license key configured.', 'wp-fedistream' ),
+ 'data' => array(
+ 'status' => 'unchecked',
+ ),
+ );
+ }
+
+ try {
+ $result = $this->client->status( $license_key );
+
+ $status_map = array(
+ LicenseState::Active->value => 'valid',
+ LicenseState::Inactive->value => 'inactive',
+ LicenseState::Expired->value => 'expired',
+ LicenseState::Revoked->value => 'revoked',
+ );
+
+ $status = $status_map[ $result->status->value ] ?? 'invalid';
+
+ $data = array(
+ 'status' => $status,
+ 'valid' => $result->valid,
+ 'domain' => $result->domain,
+ 'expires_at' => $result->expiresAt?->format( 'Y-m-d' ),
+ 'lifetime' => $result->isLifetime(),
+ 'activations_count' => $result->activationsCount,
+ 'max_activations' => $result->maxActivations,
+ );
+
+ // Cache the result.
+ $this->cache_validation( array(
+ 'success' => $result->valid,
+ 'message' => $result->valid
+ ? __( 'License is active.', 'wp-fedistream' )
+ : __( 'License is not active.', 'wp-fedistream' ),
+ 'data' => $data,
+ ) );
+
+ $this->update_cached_status( $status, $data );
+
+ return array(
+ 'success' => $result->valid,
+ 'message' => $result->valid
+ ? __( 'License is active.', 'wp-fedistream' )
+ : __( 'License is not active.', 'wp-fedistream' ),
+ 'data' => $data,
+ );
+ } catch ( LicenseNotFoundException $e ) {
+ $this->update_cached_status( 'invalid' );
+ return array(
+ 'success' => false,
+ 'message' => __( 'License key not found.', 'wp-fedistream' ),
+ 'data' => array(
+ 'status' => 'invalid',
+ ),
+ );
+ } catch ( SignatureException $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'License verification failed. Please check your server secret.', 'wp-fedistream' ),
+ 'data' => array(
+ 'status' => 'error',
+ ),
+ );
+ } catch ( \Throwable $e ) {
+ return array(
+ 'success' => false,
+ 'message' => __( 'Unable to check license status.', 'wp-fedistream' ),
+ 'data' => array(
+ 'status' => 'error',
+ ),
+ );
+ }
+ }
+
+ /**
+ * Deactivate the license (clear local data).
+ *
+ * @return bool
+ */
+ public function deactivate(): bool {
+ self::clear_license_data();
+ return true;
+ }
+
+ /**
+ * Check if the license is currently valid.
+ *
+ * Uses cached status for performance.
+ *
+ * @return bool
+ */
+ public static function is_license_valid(): bool {
+ $status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
+ return 'valid' === $status;
+ }
+
+ /**
+ * Get the license key.
+ *
+ * @return string
+ */
+ public static function get_license_key(): string {
+ return get_option( self::OPTION_LICENSE_KEY, '' );
+ }
+
+ /**
+ * Get the license server URL.
+ *
+ * @return string
+ */
+ public static function get_server_url(): string {
+ return get_option( self::OPTION_SERVER_URL, '' );
+ }
+
+ /**
+ * Get the server secret.
+ *
+ * @return string
+ */
+ public static function get_server_secret(): string {
+ return get_option( self::OPTION_SERVER_SECRET, '' );
+ }
+
+ /**
+ * Get cached license status.
+ *
+ * @return string
+ */
+ public static function get_cached_status(): string {
+ return get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
+ }
+
+ /**
+ * Get cached license data.
+ *
+ * @return array
+ */
+ public static function get_cached_data(): array {
+ return get_option( self::OPTION_LICENSE_DATA, array() );
+ }
+
+ /**
+ * Get last check timestamp.
+ *
+ * @return int
+ */
+ public static function get_last_check(): int {
+ return (int) get_option( self::OPTION_LAST_CHECK, 0 );
+ }
+
+ /**
+ * Save license settings.
+ *
+ * @param array $data Settings data.
+ * @return bool
+ */
+ public static function save_settings( array $data ): bool {
+ if ( isset( $data['license_key'] ) ) {
+ update_option( self::OPTION_LICENSE_KEY, sanitize_text_field( $data['license_key'] ) );
+ }
+
+ if ( isset( $data['server_url'] ) ) {
+ update_option( self::OPTION_SERVER_URL, esc_url_raw( $data['server_url'] ) );
+ }
+
+ if ( isset( $data['server_secret'] ) ) {
+ // Only update if a new secret is provided.
+ $secret = sanitize_text_field( $data['server_secret'] );
+ if ( ! empty( $secret ) ) {
+ update_option( self::OPTION_SERVER_SECRET, $secret );
+ }
+ }
+
+ // Reset status when settings change.
+ update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
+ delete_transient( self::TRANSIENT_LICENSE_CHECK );
+
+ return true;
+ }
+
+ /**
+ * Clear all license data.
+ *
+ * @return void
+ */
+ public static function clear_license_data(): void {
+ update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
+ update_option( self::OPTION_LICENSE_DATA, array() );
+ update_option( self::OPTION_LAST_CHECK, 0 );
+ delete_transient( self::TRANSIENT_LICENSE_CHECK );
+ }
+
+ /**
+ * Update cached license status.
+ *
+ * @param string $status Status value.
+ * @param array $data Additional data.
+ * @return void
+ */
+ private function update_cached_status( string $status, array $data = array() ): void {
+ update_option( self::OPTION_LICENSE_STATUS, $status );
+ update_option( self::OPTION_LICENSE_DATA, $data );
+ update_option( self::OPTION_LAST_CHECK, time() );
+ }
+
+ /**
+ * Cache validation result.
+ *
+ * @param array $result Validation result.
+ * @return void
+ */
+ private function cache_validation( array $result ): void {
+ set_transient( self::TRANSIENT_LICENSE_CHECK, $result, self::CACHE_TTL );
+ }
+
+ /**
+ * Get cached validation result.
+ *
+ * @return array|null
+ */
+ private function get_cached_validation(): ?array {
+ $cached = get_transient( self::TRANSIENT_LICENSE_CHECK );
+ return false === $cached ? null : $cached;
+ }
+
+ /**
+ * AJAX handler: Validate license.
+ *
+ * @return void
+ */
+ public function ajax_validate_license(): void {
+ check_ajax_referer( 'fedistream_license_action', 'nonce' );
+
+ if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-fedistream' ),
+ ) );
+ }
+
+ $result = $this->validate();
+
+ if ( $result['success'] ) {
+ wp_send_json_success( $result );
+ } else {
+ wp_send_json_error( $result );
+ }
+ }
+
+ /**
+ * AJAX handler: Activate license.
+ *
+ * @return void
+ */
+ public function ajax_activate_license(): void {
+ check_ajax_referer( 'fedistream_license_action', 'nonce' );
+
+ if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-fedistream' ),
+ ) );
+ }
+
+ $result = $this->activate();
+
+ if ( $result['success'] ) {
+ wp_send_json_success( $result );
+ } else {
+ wp_send_json_error( $result );
+ }
+ }
+
+ /**
+ * AJAX handler: Deactivate license.
+ *
+ * @return void
+ */
+ public function ajax_deactivate_license(): void {
+ check_ajax_referer( 'fedistream_license_action', 'nonce' );
+
+ if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-fedistream' ),
+ ) );
+ }
+
+ $this->deactivate();
+
+ wp_send_json_success( array(
+ 'success' => true,
+ 'message' => __( 'License deactivated.', 'wp-fedistream' ),
+ ) );
+ }
+
+ /**
+ * AJAX handler: Check license status.
+ *
+ * @return void
+ */
+ public function ajax_check_status(): void {
+ check_ajax_referer( 'fedistream_license_action', 'nonce' );
+
+ if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
+ wp_send_json_error( array(
+ 'message' => __( 'You do not have permission to perform this action.', 'wp-fedistream' ),
+ ) );
+ }
+
+ $force_refresh = isset( $_POST['force'] ) && 'true' === $_POST['force'];
+ $result = $this->get_status( $force_refresh );
+
+ if ( $result['success'] ) {
+ wp_send_json_success( $result );
+ } else {
+ wp_send_json_error( $result );
+ }
+ }
+}
diff --git a/includes/Plugin.php b/includes/Plugin.php
index df8476f..5e0a9dc 100644
--- a/includes/Plugin.php
+++ b/includes/Plugin.php
@@ -27,6 +27,7 @@ use WP_FediStream\Taxonomies\License;
use WP_FediStream\User\Library as UserLibrary;
use WP_FediStream\User\LibraryPage;
use WP_FediStream\User\Notifications;
+use WP_FediStream\License\Manager as LicenseManager;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
@@ -155,10 +156,13 @@ final class Plugin {
new ListColumns();
}
- // Initialize frontend components.
- if ( ! is_admin() ) {
+ // Initialize frontend components (only if licensed).
+ if ( ! is_admin() && LicenseManager::is_license_valid() ) {
new TemplateLoader();
new Shortcodes();
+ } elseif ( ! is_admin() ) {
+ // Register shortcodes that show license message.
+ new Shortcodes( true ); // Unlicensed mode.
}
// Initialize widgets (always needed for admin widget management).
@@ -167,8 +171,8 @@ final class Plugin {
// Initialize AJAX handlers.
new Ajax();
- // Initialize ActivityPub integration.
- if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) ) {
+ // Initialize ActivityPub integration (only if licensed and enabled).
+ if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) && LicenseManager::is_license_valid() ) {
new ActivityPubIntegration();
new ActivityPubRestApi();
}
@@ -184,6 +188,9 @@ final class Plugin {
new UserLibrary();
new LibraryPage();
new Notifications();
+
+ // Initialize license manager.
+ LicenseManager::get_instance();
}
/**
@@ -409,86 +416,334 @@ final class Plugin {
return;
}
- // Save settings.
- if ( isset( $_POST['fedistream_settings_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['fedistream_settings_nonce'] ), 'fedistream_save_settings' ) ) {
- update_option( 'wp_fedistream_enable_activitypub', isset( $_POST['enable_activitypub'] ) ? 1 : 0 );
- update_option( 'wp_fedistream_enable_woocommerce', isset( $_POST['enable_woocommerce'] ) ? 1 : 0 );
- update_option( 'wp_fedistream_max_upload_size', absint( $_POST['max_upload_size'] ?? 50 ) );
- update_option( 'wp_fedistream_default_license', sanitize_text_field( wp_unslash( $_POST['default_license'] ?? 'all-rights-reserved' ) ) );
+ // Get current tab.
+ $current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'license';
- echo '' . esc_html__( 'Settings saved.', 'wp-fedistream' ) . '
';
- }
+ // Handle form submissions.
+ $this->handle_settings_save( $current_tab );
// Get current settings.
$enable_activitypub = get_option( 'wp_fedistream_enable_activitypub', 1 );
$enable_woocommerce = get_option( 'wp_fedistream_enable_woocommerce', 0 );
$max_upload_size = get_option( 'wp_fedistream_max_upload_size', 50 );
$default_license = get_option( 'wp_fedistream_default_license', 'all-rights-reserved' );
+
+ // License settings.
+ $license_key = LicenseManager::get_license_key();
+ $license_server_url = LicenseManager::get_server_url();
+ $license_status = LicenseManager::get_cached_status();
+ $license_data = LicenseManager::get_cached_data();
+ $last_check = LicenseManager::get_last_check();
+
+ $tabs = array(
+ 'license' => __( 'License', 'wp-fedistream' ),
+ 'settings' => __( 'Default Settings', 'wp-fedistream' ),
+ 'integrations' => __( 'Integrations', 'wp-fedistream' ),
+ );
?>
-
+
+ render_license_tab( $license_key, $license_server_url, $license_status, $license_data, $last_check );
+ break;
+ case 'settings':
+ $this->render_settings_tab( $max_upload_size, $default_license );
+ break;
+ case 'integrations':
+ $this->render_integrations_tab( $enable_activitypub, $enable_woocommerce );
+ break;
+ }
+ ?>
+
isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '',
+ 'server_url' => isset( $_POST['license_server_url'] ) ? esc_url_raw( wp_unslash( $_POST['license_server_url'] ) ) : '',
+ 'server_secret' => isset( $_POST['license_server_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['license_server_secret'] ) ) : '',
+ ) );
+ echo '' . esc_html__( 'License settings saved.', 'wp-fedistream' ) . '
';
+ break;
+
+ case 'settings':
+ update_option( 'wp_fedistream_max_upload_size', absint( $_POST['max_upload_size'] ?? 50 ) );
+ update_option( 'wp_fedistream_default_license', sanitize_text_field( wp_unslash( $_POST['default_license'] ?? 'all-rights-reserved' ) ) );
+ echo '' . esc_html__( 'Settings saved.', 'wp-fedistream' ) . '
';
+ break;
+
+ case 'integrations':
+ update_option( 'wp_fedistream_enable_activitypub', isset( $_POST['enable_activitypub'] ) ? 1 : 0 );
+ update_option( 'wp_fedistream_enable_woocommerce', isset( $_POST['enable_woocommerce'] ) ? 1 : 0 );
+ echo '' . esc_html__( 'Integration settings saved.', 'wp-fedistream' ) . '
';
+ break;
+ }
+ }
+
+ /**
+ * Render the License tab.
+ *
+ * @param string $license_key License key.
+ * @param string $server_url Server URL.
+ * @param string $status License status.
+ * @param array $license_data License data.
+ * @param int $last_check Last check timestamp.
+ * @return void
+ */
+ private function render_license_tab( string $license_key, string $server_url, string $status, array $license_data, int $last_check ): void {
+ $status_classes = array(
+ 'valid' => 'notice-success',
+ 'invalid' => 'notice-error',
+ 'expired' => 'notice-warning',
+ 'revoked' => 'notice-error',
+ 'inactive' => 'notice-warning',
+ 'unchecked' => 'notice-info',
+ 'unconfigured' => 'notice-info',
+ );
+
+ $status_messages = array(
+ 'valid' => __( 'License is active and valid.', 'wp-fedistream' ),
+ 'invalid' => __( 'License is invalid.', 'wp-fedistream' ),
+ 'expired' => __( 'License has expired.', 'wp-fedistream' ),
+ 'revoked' => __( 'License has been revoked.', 'wp-fedistream' ),
+ 'inactive' => __( 'License is inactive. Please activate it.', 'wp-fedistream' ),
+ 'unchecked' => __( 'License has not been validated yet.', 'wp-fedistream' ),
+ 'unconfigured' => __( 'License server is not configured.', 'wp-fedistream' ),
+ );
+
+ $status_icons = array(
+ 'valid' => 'dashicons-yes-alt',
+ 'invalid' => 'dashicons-dismiss',
+ 'expired' => 'dashicons-warning',
+ 'revoked' => 'dashicons-dismiss',
+ 'inactive' => 'dashicons-marker',
+ 'unchecked' => 'dashicons-info-outline',
+ 'unconfigured' => 'dashicons-admin-generic',
+ );
+
+ $status_class = $status_classes[ $status ] ?? 'notice-info';
+ $status_message = $status_messages[ $status ] ?? __( 'Unknown status.', 'wp-fedistream' );
+ $status_icon = $status_icons[ $status ] ?? 'dashicons-info-outline';
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 ) : ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ , YEAR.
+#
+#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: WP FediStream 0.0.1\n"
-"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-fedistream/issues\n"
-"POT-Creation-Date: 2025-01-28T00:00:00+00:00\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2025-MO-DA HO:MI+ZONE\n"
+"Project-Id-Version: WP FediStream 0.3.0\n"
+"Report-Msgid-Bugs-To: https://src.bundespruefstelle.ch/magdev/wp-fedistream/"
+"issues\n"
+"POT-Creation-Date: 2026-01-29 11:53+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
-#: wp-fedistream.php:93
-msgid "WP FediStream requires PHP version %1$s or higher. You are running PHP %2$s."
+#: includes/Admin/ListColumns.php:66 includes/Admin/ListColumns.php:148
+#: includes/WooCommerce/DigitalDelivery.php:391
+msgid "Type"
msgstr ""
-#: wp-fedistream.php:105
-msgid "WP FediStream requires WordPress version %1$s or higher. You are running WordPress %2$s."
+#: includes/Admin/ListColumns.php:67 includes/Frontend/TemplateLoader.php:503
+#: includes/User/LibraryPage.php:111 includes/Plugin.php:297
+#: includes/Plugin.php:298 includes/Plugin.php:365
+msgid "Albums"
msgstr ""
-#: wp-fedistream.php:114
-msgid "WP FediStream requires Composer dependencies to be installed. Please run \"composer install\" in the plugin directory."
+#: includes/Admin/ListColumns.php:68 includes/Admin/ListColumns.php:149
+#: includes/Admin/ListColumns.php:361 includes/Frontend/TemplateLoader.php:506
+#: includes/User/LibraryPage.php:110 includes/Plugin.php:306
+#: includes/Plugin.php:307 includes/Plugin.php:371
+msgid "Tracks"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:74 includes/Admin/ListColumns.php:155
+#: includes/Admin/ListColumns.php:256 includes/Admin/ListColumns.php:368
+msgid "Date"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:100 includes/Admin/ListColumns.php:105
+msgid "Solo"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:101 includes/PostTypes/Artist.php:147
+#: includes/Frontend/TemplateLoader.php:242 includes/User/LibraryPage.php:249
+msgid "Band"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:102 includes/PostTypes/Artist.php:148
+#: includes/Frontend/TemplateLoader.php:243
+msgid "Duo"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:103 includes/PostTypes/Artist.php:149
+#: includes/Frontend/TemplateLoader.php:244
+msgid "Collective"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:147 includes/PostTypes/Album.php:108
+#: includes/User/LibraryPage.php:249
+msgid "Artist"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:150 includes/PostTypes/Album.php:157
+msgid "Release Date"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:186 includes/Admin/ListColumns.php:298
+msgid "No artist"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:193 includes/Admin/ListColumns.php:200
+#: includes/Admin/ListColumns.php:249 includes/PostTypes/Album.php:146
+#: includes/PostTypes/Track.php:125 includes/Frontend/TemplateLoader.php:287
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:124
+msgid "Album"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:194 includes/PostTypes/Album.php:147
+#: includes/Frontend/TemplateLoader.php:288
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:125
+msgid "EP"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:195 includes/Admin/ListColumns.php:310
+#: includes/PostTypes/Album.php:148 includes/Frontend/TemplateLoader.php:289
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:126
+msgid "Single"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:196 includes/PostTypes/Album.php:149
+#: includes/Frontend/TemplateLoader.php:290
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:127
+msgid "Compilation"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:197
+msgid "Live"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:198
+msgid "Remix"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:248 includes/PostTypes/Track.php:134
+#: includes/Frontend/TemplateLoader.php:500
+#: includes/Frontend/Shortcodes.php:414 includes/User/LibraryPage.php:99
+#: includes/Plugin.php:288 includes/Plugin.php:289 includes/Plugin.php:359
+msgid "Artists"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:250 includes/Admin/ListColumns.php:362
+msgid "Duration"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:251
+msgid "Plays"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:363 includes/PostTypes/Playlist.php:280
+msgid "Visibility"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:416 includes/Admin/ListColumns.php:426
+#: includes/PostTypes/Playlist.php:282
+msgid "Public"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:417
+msgid "Unlisted"
+msgstr ""
+
+#: includes/Admin/ListColumns.php:418 includes/PostTypes/Playlist.php:284
+msgid "Private"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:43
+msgctxt "Post type general name"
+msgid "Artists"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:44
+msgctxt "Post type singular name"
+msgid "Artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:45
+msgctxt "Admin Menu text"
+msgid "Artists"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:46
+msgctxt "Add New on Toolbar"
+msgid "Artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:47 includes/PostTypes/Album.php:47
+#: includes/PostTypes/Track.php:55 includes/PostTypes/Playlist.php:47
+msgid "Add New"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:48
+msgid "Add New Artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:49
+msgid "New Artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:50
+msgid "Edit Artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:51
+msgid "View Artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:52
+msgid "All Artists"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:53
+msgid "Search Artists"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:54
+msgid "Parent Artists:"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:55
+msgid "No artists found."
+msgstr ""
+
+#: includes/PostTypes/Artist.php:56
+msgid "No artists found in Trash."
+msgstr ""
+
+#: includes/PostTypes/Artist.php:57
+msgctxt "Overrides the \"Featured Image\" phrase"
+msgid "Artist Photo"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:58
+msgctxt "Overrides the \"Set featured image\" phrase"
+msgid "Set artist photo"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:59
+msgctxt "Overrides the \"Remove featured image\" phrase"
+msgid "Remove artist photo"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:60
+msgctxt "Overrides the \"Use as featured image\" phrase"
+msgid "Use as artist photo"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:61
+msgctxt "The post type archive label"
+msgid "Artist archives"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:62
+msgctxt "Overrides the \"Insert into post\" phrase"
+msgid "Insert into artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:63
+msgctxt "Overrides the \"Uploaded to this post\" phrase"
+msgid "Uploaded to this artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:64
+msgctxt "Screen reader text"
+msgid "Filter artists list"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:65
+msgctxt "Screen reader text"
+msgid "Artists list navigation"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:66
+msgctxt "Screen reader text"
+msgid "Artists list"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:99
+msgid "Artist Information"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:108
+msgid "Social Links"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:117
+msgid "Band Members"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:142
+msgid "Artist Type"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:146 includes/Frontend/TemplateLoader.php:241
+msgid "Solo Artist"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:155
+msgid "Formed Date"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:159
+msgid "When the artist/band was formed or started their career."
+msgstr ""
+
+#: includes/PostTypes/Artist.php:164 includes/ActivityPub/ArtistActor.php:273
+msgid "Location"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:168
+msgid "City, Country or region where the artist is based."
+msgstr ""
+
+#: includes/PostTypes/Artist.php:173 includes/ActivityPub/ArtistActor.php:263
+msgid "Website"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:177
+msgid "Official website URL."
+msgstr ""
+
+#: includes/PostTypes/Artist.php:197
+msgid "Mastodon"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:198
+msgid "Bandcamp"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:199
+msgid "SoundCloud"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:200
+msgid "YouTube"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:201
+msgid "Instagram"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:202
+msgid "Twitter/X"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:203
+msgid "Facebook"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:204
+msgid "TikTok"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:205
+msgid "Other"
+msgstr ""
+
+#: includes/PostTypes/Artist.php:238
+msgid "Add band/group members (comma-separated names)."
+msgstr ""
+
+#: includes/PostTypes/Artist.php:240
+msgid "One member per line."
+msgstr ""
+
+#: includes/PostTypes/Album.php:43
+msgctxt "Post type general name"
+msgid "Albums"
+msgstr ""
+
+#: includes/PostTypes/Album.php:44
+msgctxt "Post type singular name"
+msgid "Album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:45
+msgctxt "Admin Menu text"
+msgid "Albums"
+msgstr ""
+
+#: includes/PostTypes/Album.php:46
+msgctxt "Add New on Toolbar"
+msgid "Album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:48
+msgid "Add New Album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:49
+msgid "New Album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:50
+msgid "Edit Album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:51
+msgid "View Album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:52
+msgid "All Albums"
+msgstr ""
+
+#: includes/PostTypes/Album.php:53
+msgid "Search Albums"
+msgstr ""
+
+#: includes/PostTypes/Album.php:54
+msgid "Parent Albums:"
+msgstr ""
+
+#: includes/PostTypes/Album.php:55
+msgid "No albums found."
+msgstr ""
+
+#: includes/PostTypes/Album.php:56
+msgid "No albums found in Trash."
+msgstr ""
+
+#: includes/PostTypes/Album.php:57
+msgctxt "Overrides the \"Featured Image\" phrase"
+msgid "Album Artwork"
+msgstr ""
+
+#: includes/PostTypes/Album.php:58
+msgctxt "Overrides the \"Set featured image\" phrase"
+msgid "Set album artwork"
+msgstr ""
+
+#: includes/PostTypes/Album.php:59
+msgctxt "Overrides the \"Remove featured image\" phrase"
+msgid "Remove album artwork"
+msgstr ""
+
+#: includes/PostTypes/Album.php:60
+msgctxt "Overrides the \"Use as featured image\" phrase"
+msgid "Use as album artwork"
+msgstr ""
+
+#: includes/PostTypes/Album.php:61
+msgctxt "The post type archive label"
+msgid "Album archives"
+msgstr ""
+
+#: includes/PostTypes/Album.php:62
+msgctxt "Overrides the \"Insert into post\" phrase"
+msgid "Insert into album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:63
+msgctxt "Overrides the \"Uploaded to this post\" phrase"
+msgid "Uploaded to this album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:64
+msgctxt "Screen reader text"
+msgid "Filter albums list"
+msgstr ""
+
+#: includes/PostTypes/Album.php:65
+msgctxt "Screen reader text"
+msgid "Albums list navigation"
+msgstr ""
+
+#: includes/PostTypes/Album.php:66
+msgctxt "Screen reader text"
+msgid "Albums list"
+msgstr ""
+
+#: includes/PostTypes/Album.php:99
+msgid "Album Information"
+msgstr ""
+
+#: includes/PostTypes/Album.php:117
+msgid "Album Codes"
+msgstr ""
+
+#: includes/PostTypes/Album.php:142
+msgid "Release Type"
+msgstr ""
+
+#: includes/PostTypes/Album.php:150 includes/Frontend/TemplateLoader.php:291
+msgid "Live Album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:151 includes/Frontend/TemplateLoader.php:292
+msgid "Remix Album"
+msgstr ""
+
+#: includes/PostTypes/Album.php:165
+msgid "Total Tracks"
+msgstr ""
+
+#: includes/PostTypes/Album.php:169
+msgid "Auto-calculated when tracks are added."
+msgstr ""
+
+#: includes/PostTypes/Album.php:174
+msgid "Total Duration"
+msgstr ""
+
+#: includes/PostTypes/Album.php:178
+msgid "seconds (auto-calculated)"
+msgstr ""
+
+#: includes/PostTypes/Album.php:205
+msgid "Primary Artist"
+msgstr ""
+
+#: includes/PostTypes/Album.php:208
+msgid "— Select Artist —"
+msgstr ""
+
+#: includes/PostTypes/Album.php:220 includes/PostTypes/Track.php:479
+#, php-format
+msgid "No artists found. %s"
+msgstr ""
+
+#: includes/PostTypes/Album.php:221 includes/PostTypes/Track.php:480
+msgid "Add an artist first."
+msgstr ""
+
+#: includes/PostTypes/Album.php:240
+msgid "UPC/EAN"
+msgstr ""
+
+#: includes/PostTypes/Album.php:241
+msgid "12-13 digit UPC/EAN code"
+msgstr ""
+
+#: includes/PostTypes/Album.php:244
+msgid "Catalog Number"
+msgstr ""
+
+#: includes/PostTypes/Track.php:51
+msgctxt "Post type general name"
+msgid "Tracks"
+msgstr ""
+
+#: includes/PostTypes/Track.php:52
+msgctxt "Post type singular name"
+msgid "Track"
+msgstr ""
+
+#: includes/PostTypes/Track.php:53
+msgctxt "Admin Menu text"
+msgid "Tracks"
+msgstr ""
+
+#: includes/PostTypes/Track.php:54
+msgctxt "Add New on Toolbar"
+msgid "Track"
+msgstr ""
+
+#: includes/PostTypes/Track.php:56
+msgid "Add New Track"
+msgstr ""
+
+#: includes/PostTypes/Track.php:57
+msgid "New Track"
+msgstr ""
+
+#: includes/PostTypes/Track.php:58
+msgid "Edit Track"
+msgstr ""
+
+#: includes/PostTypes/Track.php:59
+msgid "View Track"
+msgstr ""
+
+#: includes/PostTypes/Track.php:60
+msgid "All Tracks"
+msgstr ""
+
+#: includes/PostTypes/Track.php:61
+msgid "Search Tracks"
+msgstr ""
+
+#: includes/PostTypes/Track.php:62
+msgid "Parent Tracks:"
+msgstr ""
+
+#: includes/PostTypes/Track.php:63
+msgid "No tracks found."
+msgstr ""
+
+#: includes/PostTypes/Track.php:64
+msgid "No tracks found in Trash."
+msgstr ""
+
+#: includes/PostTypes/Track.php:65
+msgctxt "Overrides the \"Featured Image\" phrase"
+msgid "Track Artwork"
+msgstr ""
+
+#: includes/PostTypes/Track.php:66
+msgctxt "Overrides the \"Set featured image\" phrase"
+msgid "Set track artwork"
+msgstr ""
+
+#: includes/PostTypes/Track.php:67
+msgctxt "Overrides the \"Remove featured image\" phrase"
+msgid "Remove track artwork"
+msgstr ""
+
+#: includes/PostTypes/Track.php:68
+msgctxt "Overrides the \"Use as featured image\" phrase"
+msgid "Use as track artwork"
+msgstr ""
+
+#: includes/PostTypes/Track.php:69
+msgctxt "The post type archive label"
+msgid "Track archives"
+msgstr ""
+
+#: includes/PostTypes/Track.php:70
+msgctxt "Overrides the \"Insert into post\" phrase"
+msgid "Insert into track"
+msgstr ""
+
+#: includes/PostTypes/Track.php:71
+msgctxt "Overrides the \"Uploaded to this post\" phrase"
+msgid "Uploaded to this track"
+msgstr ""
+
+#: includes/PostTypes/Track.php:72
+msgctxt "Screen reader text"
+msgid "Filter tracks list"
+msgstr ""
+
+#: includes/PostTypes/Track.php:73
+msgctxt "Screen reader text"
+msgid "Tracks list navigation"
+msgstr ""
+
+#: includes/PostTypes/Track.php:74
+msgctxt "Screen reader text"
+msgid "Tracks list"
+msgstr ""
+
+#: includes/PostTypes/Track.php:107 includes/PostTypes/Track.php:168
+msgid "Audio File"
+msgstr ""
+
+#: includes/PostTypes/Track.php:116
+msgid "Track Information"
+msgstr ""
+
+#: includes/PostTypes/Track.php:143
+msgid "Track Codes"
+msgstr ""
+
+#: includes/PostTypes/Track.php:185 includes/PostTypes/Track.php:241
+msgid "Select Audio File"
+msgstr ""
+
+#: includes/PostTypes/Track.php:186 includes/PostTypes/Playlist.php:184
+#: includes/PostTypes/Playlist.php:252
+msgid "Remove"
+msgstr ""
+
+#: includes/PostTypes/Track.php:188
+msgid "Supported formats: MP3, WAV, FLAC, OGG"
+msgstr ""
+
+#: includes/PostTypes/Track.php:194
+msgid "Audio Format"
+msgstr ""
+
+#: includes/PostTypes/Track.php:198
+msgid "— Auto-detect —"
+msgstr ""
+
+#: includes/PostTypes/Track.php:208
+msgid "Duration (seconds)"
+msgstr ""
+
+#: includes/PostTypes/Track.php:217
+#, php-format
+msgid "(%s)"
+msgstr ""
+
+#: includes/PostTypes/Track.php:223
+msgid "Auto-detected from audio file if available."
+msgstr ""
+
+#: includes/PostTypes/Track.php:242
+msgid "Use this file"
+msgstr ""
+
+#: includes/PostTypes/Track.php:305
+msgid "— Select Key —"
+msgstr ""
+
+#: includes/PostTypes/Track.php:335
+msgid "Track Number"
+msgstr ""
+
+#: includes/PostTypes/Track.php:343
+msgid "Disc Number"
+msgstr ""
+
+#: includes/PostTypes/Track.php:351
+msgid "BPM"
+msgstr ""
+
+#: includes/PostTypes/Track.php:355
+msgid "Beats per minute (tempo)."
+msgstr ""
+
+#: includes/PostTypes/Track.php:360
+msgid "Musical Key"
+msgstr ""
+
+#: includes/PostTypes/Track.php:372
+msgid "Explicit Content"
+msgstr ""
+
+#: includes/PostTypes/Track.php:377
+msgid "This track contains explicit content"
+msgstr ""
+
+#: includes/PostTypes/Track.php:383
+msgid "Preview Settings"
+msgstr ""
+
+#: includes/PostTypes/Track.php:387
+msgid "Start at:"
+msgstr ""
+
+#: includes/PostTypes/Track.php:389 includes/PostTypes/Track.php:395
+msgid "seconds"
+msgstr ""
+
+#: includes/PostTypes/Track.php:393
+msgid "Duration:"
+msgstr ""
+
+#: includes/PostTypes/Track.php:397
+msgid "Preview clip for non-authenticated users or before purchase."
+msgstr ""
+
+#: includes/PostTypes/Track.php:425
+msgid "— No Album (Single) —"
+msgstr ""
+
+#: includes/PostTypes/Track.php:465
+msgid "Select all artists featured on this track."
+msgstr ""
+
+#: includes/PostTypes/Track.php:498
+msgid "ISRC"
+msgstr ""
+
+#: includes/PostTypes/Track.php:499
+msgid "ISRC format: CC-XXX-YY-NNNNN"
+msgstr ""
+
+#: includes/PostTypes/Track.php:500
+msgid "International Standard Recording Code"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:43
+msgctxt "Post type general name"
+msgid "Playlists"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:44
+msgctxt "Post type singular name"
+msgid "Playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:45
+msgctxt "Admin Menu text"
+msgid "Playlists"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:46
+msgctxt "Add New on Toolbar"
+msgid "Playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:48
+msgid "Add New Playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:49
+msgid "New Playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:50
+msgid "Edit Playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:51
+msgid "View Playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:52
+msgid "All Playlists"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:53
+msgid "Search Playlists"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:54
+msgid "Parent Playlists:"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:55
+msgid "No playlists found."
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:56
+msgid "No playlists found in Trash."
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:57
+msgctxt "Overrides the \"Featured Image\" phrase"
+msgid "Playlist Cover"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:58
+msgctxt "Overrides the \"Set featured image\" phrase"
+msgid "Set playlist cover"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:59
+msgctxt "Overrides the \"Remove featured image\" phrase"
+msgid "Remove playlist cover"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:60
+msgctxt "Overrides the \"Use as featured image\" phrase"
+msgid "Use as playlist cover"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:61
+msgctxt "The post type archive label"
+msgid "Playlist archives"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:62
+msgctxt "Overrides the \"Insert into post\" phrase"
+msgid "Insert into playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:63
+msgctxt "Overrides the \"Uploaded to this post\" phrase"
+msgid "Uploaded to this playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:64
+msgctxt "Screen reader text"
+msgid "Filter playlists list"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:65
+msgctxt "Screen reader text"
+msgid "Playlists list navigation"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:66
+msgctxt "Screen reader text"
+msgid "Playlists list"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:99
+msgid "Playlist Tracks"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:108
+msgid "Playlist Settings"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:117
+msgid "Playlist Stats"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:160
+msgid "Current Tracks"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:192
+msgid "Add Tracks"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:195
+msgid "— Select a track to add —"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:211
+msgid "Add to Playlist"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:283
+msgid "Unlisted (link only)"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:290
+msgid "Collaborative"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:293
+msgid "Allow others to add tracks."
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:298
+msgid "Federated"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:301
+msgid "Allow tracks from other FediStream instances."
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:317
+msgid "Tracks:"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:321
+msgid "Total Duration:"
+msgstr ""
+
+#: includes/PostTypes/Playlist.php:330
+msgid "Stats are automatically updated when saved."
+msgstr ""
+
+#: includes/ActivityPub/ArtistActor.php:300
+msgid "Active Since"
+msgstr ""
+
+#: includes/ActivityPub/AlbumTransformer.php:111
+#, php-format
+msgid "By %s"
+msgstr ""
+
+#: includes/ActivityPub/AlbumTransformer.php:115
+#, php-format
+msgid "- Released %s"
+msgstr ""
+
+#: includes/ActivityPub/AlbumTransformer.php:119
+#, php-format
+msgid "- %d track"
+msgid_plural "- %d tracks"
+msgstr[0] ""
+msgstr[1] ""
+
+#: includes/ActivityPub/RestApi.php:438
+msgid "Published to ActivityPub"
+msgstr ""
+
+#: includes/ActivityPub/RestApi.php:446
+msgid "Failed to publish"
+msgstr ""
+
+#: includes/WooCommerce/AlbumProduct.php:371
+#: includes/WooCommerce/TrackProduct.php:394
+#, php-format
+msgid "%1$s (%2$s)"
+msgstr ""
+
+#: includes/WooCommerce/AlbumProduct.php:419
+#: includes/WooCommerce/TrackProduct.php:442
+msgid "Name Your Price"
+msgstr ""
+
+#: includes/WooCommerce/AlbumProduct.php:431
+#: includes/WooCommerce/TrackProduct.php:454
+#, php-format
+msgid "From %s"
+msgstr ""
+
+#: includes/WooCommerce/AlbumProduct.php:435
+#: includes/WooCommerce/TrackProduct.php:458
+#: includes/WooCommerce/Integration.php:246
+msgid "Pay What You Want"
+msgstr ""
+
+#: includes/WooCommerce/AlbumProduct.php:442
+#: includes/WooCommerce/TrackProduct.php:465
+#, php-format
+msgid "(Suggested: %s)"
+msgstr ""
+
+#: includes/WooCommerce/AlbumProduct.php:485
+#: includes/WooCommerce/TrackProduct.php:507
+#, php-format
+msgid "Please enter at least %s"
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:52
+msgid "You must be logged in to download files."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:75
+#: includes/WooCommerce/StreamingAccess.php:239
+msgid "Invalid track."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:80
+msgid "You have not purchased this track."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:85
+#: includes/WooCommerce/StreamingAccess.php:244 includes/Frontend/Ajax.php:60
+#: includes/Frontend/Ajax.php:154
+msgid "Track not found."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:91
+#: includes/WooCommerce/StreamingAccess.php:250 includes/Frontend/Ajax.php:73
+msgid "No audio file available."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:96
+#: includes/WooCommerce/DigitalDelivery.php:285
+#: includes/WooCommerce/StreamingAccess.php:255
+msgid "File not found."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:120
+msgid "Invalid album."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:125
+msgid "You have not purchased this album."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:130
+msgid "Album not found."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:152
+msgid "No tracks found in this album."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:159
+msgid "Failed to create download package."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:350
+#: includes/WooCommerce/DigitalDelivery.php:354
+msgid "Your FediStream Downloads"
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:351
+msgid "Access your purchased music at:"
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:359
+#, php-format
+msgid "Access your purchased music in your %s."
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:360
+msgid "account downloads"
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:386
+msgid "FediStream Library"
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:390
+msgid "Title"
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:392
+msgid "Purchased"
+msgstr ""
+
+#: includes/WooCommerce/DigitalDelivery.php:393
+msgid "Download"
+msgstr ""
+
+#: includes/WooCommerce/StreamingAccess.php:373
+msgid "You own this track."
+msgstr ""
+
+#: includes/WooCommerce/StreamingAccess.php:378
+msgid "You own this album."
+msgstr ""
+
+#: includes/WooCommerce/StreamingAccess.php:391
+msgid "Buy Track"
+msgstr ""
+
+#: includes/WooCommerce/StreamingAccess.php:408
+#, php-format
+msgid "Buy Full Album: %s"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:115
+msgid "FediStream Album"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:116
+msgid "FediStream Track"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:148
+#: includes/WooCommerce/Integration.php:701 includes/Plugin.php:266
+#: includes/Plugin.php:267
+msgid "FediStream"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:155
+msgid "Audio Formats"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:213
+msgid "Linked Album"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:215
+msgid "Select an album..."
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:222
+msgid "Select the FediStream album this product represents."
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:228
+msgid "Linked Track"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:230
+msgid "Select a track..."
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:237
+msgid "Select the FediStream track this product represents."
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:243
+msgid "Pricing Type"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:245
+msgid "Fixed Price"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:247
+msgid "Name Your Price (Free+)"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:257
+msgid "Minimum Price"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:259
+msgid "Minimum price for Pay What You Want. Leave empty for no minimum."
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:269
+msgid "Suggested Price"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:271
+msgid "Suggested price shown to customers."
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:285
+msgid "Include Streaming"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:286
+msgid "Purchase unlocks full-quality streaming access."
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:297
+msgid "Available Formats"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:319
+msgid "Select which audio formats customers can download after purchase."
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:446
+msgid "Preview"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:448
+msgid "Play preview"
+msgstr ""
+
+#: includes/WooCommerce/Integration.php:492
+msgid "Tracklist"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:47
+msgctxt "taxonomy general name"
+msgid "Genres"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:48
+msgctxt "taxonomy singular name"
+msgid "Genre"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:49
+msgid "Search Genres"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:50
+msgid "Popular Genres"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:51
+msgid "All Genres"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:52
+msgid "Parent Genre"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:53
+msgid "Parent Genre:"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:54
+msgid "Edit Genre"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:55
+msgid "View Genre"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:56
+msgid "Update Genre"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:57
+msgid "Add New Genre"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:58
+msgid "New Genre Name"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:59
+msgid "Separate genres with commas"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:60
+msgid "Add or remove genres"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:61
+msgid "Choose from the most used genres"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:62
+msgid "No genres found."
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:63
+msgid "No genres"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:64 includes/Plugin.php:324
+#: includes/Plugin.php:325
+msgid "Genres"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:65
+msgid "Genres list navigation"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:66
+msgid "Genres list"
+msgstr ""
+
+#: includes/Taxonomies/Genre.php:67
+msgid "← Back to Genres"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:46
+msgctxt "taxonomy general name"
+msgid "Moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:47
+msgctxt "taxonomy singular name"
+msgid "Mood"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:48
+msgid "Search Moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:49
+msgid "Popular Moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:50
+msgid "All Moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:51
+msgid "Edit Mood"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:52
+msgid "View Mood"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:53
+msgid "Update Mood"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:54
+msgid "Add New Mood"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:55
+msgid "New Mood Name"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:56
+msgid "Separate moods with commas"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:57
+msgid "Add or remove moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:58
+msgid "Choose from the most used moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:59
+msgid "No moods found."
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:60
+msgid "No moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:61
+msgid "Moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:62
+msgid "Moods list navigation"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:63
+msgid "Moods list"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:64
+msgid "← Back to Moods"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:98
+msgid "Energetic"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:99
+msgid "Calm"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:100
+msgid "Uplifting"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:101
+msgid "Melancholic"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:102
+msgid "Aggressive"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:103
+msgid "Romantic"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:104
+msgid "Happy"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:105
+msgid "Sad"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:106
+msgid "Relaxing"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:107
+msgid "Intense"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:108
+msgid "Dreamy"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:109
+msgid "Dark"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:110
+msgid "Groovy"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:111
+msgid "Epic"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:112
+msgid "Peaceful"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:113
+msgid "Motivational"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:114
+msgid "Nostalgic"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:115
+msgid "Playful"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:116
+msgid "Sensual"
+msgstr ""
+
+#: includes/Taxonomies/Mood.php:117
+msgid "Suspenseful"
+msgstr ""
+
+#: includes/Taxonomies/License.php:46
+msgctxt "taxonomy general name"
+msgid "Licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:47
+msgctxt "taxonomy singular name"
+msgid "License"
+msgstr ""
+
+#: includes/Taxonomies/License.php:48
+msgid "Search Licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:49
+msgid "Popular Licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:50
+msgid "All Licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:51
+msgid "Parent License"
+msgstr ""
+
+#: includes/Taxonomies/License.php:52
+msgid "Parent License:"
+msgstr ""
+
+#: includes/Taxonomies/License.php:53
+msgid "Edit License"
+msgstr ""
+
+#: includes/Taxonomies/License.php:54
+msgid "View License"
+msgstr ""
+
+#: includes/Taxonomies/License.php:55
+msgid "Update License"
+msgstr ""
+
+#: includes/Taxonomies/License.php:56
+msgid "Add New License"
+msgstr ""
+
+#: includes/Taxonomies/License.php:57
+msgid "New License Name"
+msgstr ""
+
+#: includes/Taxonomies/License.php:58
+msgid "Separate licenses with commas"
+msgstr ""
+
+#: includes/Taxonomies/License.php:59
+msgid "Add or remove licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:60
+msgid "Choose from the most used licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:61
+msgid "No licenses found."
+msgstr ""
+
+#: includes/Taxonomies/License.php:62
+msgid "No licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:63
+msgid "Licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:64
+msgid "Licenses list navigation"
+msgstr ""
+
+#: includes/Taxonomies/License.php:65
+msgid "Licenses list"
+msgstr ""
+
+#: includes/Taxonomies/License.php:66
+msgid "← Back to Licenses"
+msgstr ""
+
+#: includes/Taxonomies/License.php:101
+msgid "Standard copyright. All rights reserved by the creator."
+msgstr ""
+
+#: includes/Taxonomies/License.php:105
+msgid "Creative Commons licenses for sharing and reuse."
+msgstr ""
+
+#: includes/Taxonomies/License.php:107
+msgid "Public Domain Dedication - No rights reserved"
+msgstr ""
+
+#: includes/Taxonomies/License.php:108
+msgid "Attribution - Credit must be given"
+msgstr ""
+
+#: includes/Taxonomies/License.php:109
+msgid "Attribution-ShareAlike - Credit and share under same terms"
+msgstr ""
+
+#: includes/Taxonomies/License.php:110
+msgid "Attribution-NoDerivs - Credit, no modifications"
+msgstr ""
+
+#: includes/Taxonomies/License.php:111
+msgid "Attribution-NonCommercial - Credit, non-commercial only"
+msgstr ""
+
+#: includes/Taxonomies/License.php:112
+msgid "Attribution-NonCommercial-ShareAlike"
+msgstr ""
+
+#: includes/Taxonomies/License.php:113
+msgid "Attribution-NonCommercial-NoDerivs"
+msgstr ""
+
+#: includes/Taxonomies/License.php:117
+msgid "Works in the public domain with no copyright restrictions."
+msgstr ""
+
+#: includes/Roles/Capabilities.php:224
+msgid "FediStream Artist"
+msgstr ""
+
+#: includes/Roles/Capabilities.php:231
+msgid "FediStream Label"
+msgstr ""
+
+#: includes/Frontend/TemplateLoader.php:486
+msgid "« Previous"
+msgstr ""
+
+#: includes/Frontend/TemplateLoader.php:487
+msgid "Next »"
+msgstr ""
+
+#: includes/Frontend/TemplateLoader.php:509 includes/User/LibraryPage.php:112
+#: includes/Plugin.php:315 includes/Plugin.php:316 includes/Plugin.php:377
+msgid "Playlists"
+msgstr ""
+
+#: includes/Frontend/template-wrapper.php:40
+msgid "Genre"
+msgstr ""
+
+#: includes/Frontend/template-wrapper.php:43
+msgid "Mood"
+msgstr ""
+
+#: includes/Frontend/template-wrapper.php:61
+msgid "Template Error:"
+msgstr ""
+
+#: includes/Frontend/Shortcodes.php:55
+msgid "This content requires a valid FediStream license."
+msgstr ""
+
+#: includes/Frontend/Shortcodes.php:280
+msgid "Latest Releases"
+msgstr ""
+
+#: includes/Frontend/Shortcodes.php:347
+#: includes/Frontend/Widgets/PopularTracksWidget.php:45
+#: includes/Frontend/Widgets/PopularTracksWidget.php:100
+msgid "Popular Tracks"
+msgstr ""
+
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:29
+msgid "FediStream: Recent Releases"
+msgstr ""
+
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:31
+msgid "Display recent album releases."
+msgstr ""
+
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:45
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:108
+msgid "Recent Releases"
+msgstr ""
+
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:113
+#: includes/Frontend/Widgets/PopularTracksWidget.php:104
+#: includes/Frontend/Widgets/FeaturedArtistWidget.php:125
+#: includes/Frontend/Widgets/NowPlayingWidget.php:83
+msgid "Title:"
+msgstr ""
+
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:117
+msgid "Number of releases:"
+msgstr ""
+
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:121
+msgid "Release type:"
+msgstr ""
+
+#: includes/Frontend/Widgets/RecentReleasesWidget.php:123
+msgid "All types"
+msgstr ""
+
+#: includes/Frontend/Widgets/PopularTracksWidget.php:29
+msgid "FediStream: Popular Tracks"
+msgstr ""
+
+#: includes/Frontend/Widgets/PopularTracksWidget.php:31
+msgid "Display popular tracks by play count."
+msgstr ""
+
+#: includes/Frontend/Widgets/PopularTracksWidget.php:108
+msgid "Number of tracks:"
+msgstr ""
+
+#: includes/Frontend/Widgets/FeaturedArtistWidget.php:29
+msgid "FediStream: Featured Artist"
+msgstr ""
+
+#: includes/Frontend/Widgets/FeaturedArtistWidget.php:31
+msgid "Display a featured artist."
+msgstr ""
+
+#: includes/Frontend/Widgets/FeaturedArtistWidget.php:45
+#: includes/Frontend/Widgets/FeaturedArtistWidget.php:109
+msgid "Featured Artist"
+msgstr ""
+
+#: includes/Frontend/Widgets/FeaturedArtistWidget.php:131
+msgid "Show random artist"
+msgstr ""
+
+#: includes/Frontend/Widgets/FeaturedArtistWidget.php:135
+msgid "Or select specific artist:"
+msgstr ""
+
+#: includes/Frontend/Widgets/FeaturedArtistWidget.php:137
+msgid "-- Select Artist --"
+msgstr ""
+
+#: includes/Frontend/Widgets/NowPlayingWidget.php:28
+msgid "FediStream: Now Playing"
+msgstr ""
+
+#: includes/Frontend/Widgets/NowPlayingWidget.php:30
+msgid "Display the currently playing track."
+msgstr ""
+
+#: includes/Frontend/Widgets/NowPlayingWidget.php:44
+#: includes/Frontend/Widgets/NowPlayingWidget.php:79
+msgid "Now Playing"
+msgstr ""
+
+#: includes/Frontend/Widgets/NowPlayingWidget.php:89
+msgid "Show player controls"
+msgstr ""
+
+#: includes/Frontend/Widgets/NowPlayingWidget.php:93
+msgid ""
+"This widget shows information about the currently playing track and updates "
+"automatically via JavaScript."
+msgstr ""
+
+#: includes/Frontend/Ajax.php:43 includes/Frontend/Ajax.php:137
+msgid "This feature requires a valid license."
+msgstr ""
+
+#: includes/Frontend/Ajax.php:48 includes/Frontend/Ajax.php:142
+msgid "Invalid nonce."
+msgstr ""
+
+#: includes/Frontend/Ajax.php:54 includes/Frontend/Ajax.php:148
+msgid "Invalid track ID."
+msgstr ""
+
+#: includes/Frontend/Ajax.php:65
+msgid "Track not available."
+msgstr ""
+
+#: includes/Frontend/Ajax.php:179
+msgid "Play recorded."
+msgstr ""
+
+#: includes/User/Library.php:49 includes/User/Library.php:94
+#: includes/User/Library.php:138 includes/User/Library.php:160
+#: includes/User/Library.php:181 includes/User/Library.php:202
+msgid "You must be logged in."
+msgstr ""
+
+#: includes/User/Library.php:56
+msgid "Invalid request."
+msgstr ""
+
+#: includes/User/Library.php:76
+msgid "Added to your library."
+msgstr ""
+
+#: includes/User/Library.php:77
+msgid "Removed from your library."
+msgstr ""
+
+#: includes/User/Library.php:81
+msgid "Failed to update library."
+msgstr ""
+
+#: includes/User/Library.php:100
+msgid "Invalid artist."
+msgstr ""
+
+#: includes/User/Library.php:120
+msgid "You are now following this artist."
+msgstr ""
+
+#: includes/User/Library.php:121
+msgid "You unfollowed this artist."
+msgstr ""
+
+#: includes/User/Library.php:125
+msgid "Failed to update follow status."
+msgstr ""
+
+#: includes/User/Library.php:209 includes/User/LibraryPage.php:58
+msgid "History cleared."
+msgstr ""
+
+#: includes/User/Library.php:211
+msgid "Failed to clear history."
+msgstr ""
+
+#: includes/User/Library.php:707 includes/User/LibraryPage.php:216
+msgid "Remove from library"
+msgstr ""
+
+#: includes/User/Library.php:707
+msgid "Add to library"
+msgstr ""
+
+#: includes/User/Library.php:736
+msgid "Following"
+msgstr ""
+
+#: includes/User/Library.php:736
+msgid "Follow"
+msgstr ""
+
+#: includes/User/LibraryPage.php:53
+msgid "Loading..."
+msgstr ""
+
+#: includes/User/LibraryPage.php:54
+msgid "No favorites yet."
+msgstr ""
+
+#: includes/User/LibraryPage.php:55
+msgid "Not following any artists yet."
+msgstr ""
+
+#: includes/User/LibraryPage.php:56
+msgid "No listening history."
+msgstr ""
+
+#: includes/User/LibraryPage.php:57
+msgid "Are you sure you want to clear your listening history?"
+msgstr ""
+
+#: includes/User/LibraryPage.php:59
+msgid "An error occurred. Please try again."
+msgstr ""
+
+#: includes/User/LibraryPage.php:95
+msgid "Favorites"
+msgstr ""
+
+#: includes/User/LibraryPage.php:103
+msgid "History"
+msgstr ""
+
+#: includes/User/LibraryPage.php:109
+msgid "All"
+msgstr ""
+
+#: includes/User/LibraryPage.php:134
+msgid "Clear History"
+msgstr ""
+
+#: includes/User/LibraryPage.php:161
+msgid "Please log in to view your library."
+msgstr ""
+
+#: includes/User/LibraryPage.php:163
+msgid "Log In"
+msgstr ""
+
+#: includes/User/LibraryPage.php:208
+#, php-format
+msgid "%d track"
+msgid_plural "%d tracks"
+msgstr[0] ""
+msgstr[1] ""
+
+#: includes/User/LibraryPage.php:253 includes/User/LibraryPage.php:255
+msgid "Unfollow"
+msgstr ""
+
+#: includes/User/LibraryPage.php:296
+#, php-format
+msgid "Played %s"
+msgstr ""
+
+#: includes/User/LibraryPage.php:297 includes/Plugin.php:581
+msgid "ago"
+msgstr ""
+
+#: includes/User/Notifications.php:84
+msgid "No notifications"
+msgstr ""
+
+#: includes/User/Notifications.php:85
+msgid "Mark all as read"
+msgstr ""
+
+#: includes/User/Notifications.php:86
+msgid "View all notifications"
+msgstr ""
+
+#: includes/User/Notifications.php:87
+msgid "Just now"
+msgstr ""
+
+#: includes/User/Notifications.php:88
+msgid "An error occurred."
+msgstr ""
+
+#: includes/User/Notifications.php:309 includes/User/Notifications.php:337
+#: includes/User/Notifications.php:369 includes/User/Notifications.php:391
+msgid "Not logged in."
+msgstr ""
+
+#: includes/User/Notifications.php:343 includes/User/Notifications.php:397
+msgid "Invalid notification."
+msgstr ""
+
+#: includes/User/Notifications.php:356
+msgid "Failed to update notification."
+msgstr ""
+
+#: includes/User/Notifications.php:378
+msgid "Failed to update notifications."
+msgstr ""
+
+#: includes/User/Notifications.php:410
+msgid "Failed to delete notification."
+msgstr ""
+
+#: includes/User/Notifications.php:445
+#, php-format
+msgid "New release from %s"
+msgstr ""
+
+#: includes/User/Notifications.php:450
+#, php-format
+msgid "%1$s released a new album: %2$s"
+msgstr ""
+
+#: includes/User/Notifications.php:509
+#, php-format
+msgid "New track from %s"
+msgstr ""
+
+#: includes/User/Notifications.php:514
+#, php-format
+msgid "%1$s released a new track: %2$s"
+msgstr ""
+
+#: includes/User/Notifications.php:555
+msgid "New follower"
+msgstr ""
+
+#: includes/User/Notifications.php:558
+#, php-format
+msgid "%s started following you"
+msgstr ""
+
+#: includes/User/Notifications.php:586 includes/User/Notifications.php:626
+msgid "Someone"
+msgstr ""
+
+#: includes/User/Notifications.php:591
+msgid "New like from Fediverse"
+msgstr ""
+
+#: includes/User/Notifications.php:594
+#, php-format
+msgid "%1$s liked your %2$s"
+msgstr ""
+
+#: includes/User/Notifications.php:631
+msgid "New boost from Fediverse"
+msgstr ""
+
+#: includes/User/Notifications.php:634
+#, php-format
+msgid "%1$s boosted your %2$s"
+msgstr ""
+
+#: includes/User/Notifications.php:678
+#, php-format
+msgid "[%1$s] %2$s"
+msgstr ""
+
+#: includes/User/Notifications.php:720
+msgid "View Details"
+msgstr ""
+
+#: includes/User/Notifications.php:726
+#, php-format
+msgid "This email was sent by %s."
+msgstr ""
+
+#: includes/Plugin.php:221 includes/Plugin.php:333 includes/Plugin.php:334
+msgid "Settings"
+msgstr ""
+
+#: includes/Plugin.php:227 includes/Plugin.php:278 includes/Plugin.php:279
+msgid "Dashboard"
+msgstr ""
+
+#: includes/Plugin.php:354
+msgid "FediStream Dashboard"
+msgstr ""
+
+#: includes/Plugin.php:361
+msgid "Manage Artists"
+msgstr ""
+
+#: includes/Plugin.php:367
+msgid "Manage Albums"
+msgstr ""
+
+#: includes/Plugin.php:373
+msgid "Manage Tracks"
+msgstr ""
+
+#: includes/Plugin.php:379
+msgid "Manage Playlists"
+msgstr ""
+
+#: includes/Plugin.php:384
+msgid "Quick Actions"
+msgstr ""
+
+#: includes/Plugin.php:386
+msgid "Add Artist"
+msgstr ""
+
+#: includes/Plugin.php:387
+msgid "Add Album"
+msgstr ""
+
+#: includes/Plugin.php:388
+msgid "Add Track"
+msgstr ""
+
+#: includes/Plugin.php:389
+msgid "Add Playlist"
+msgstr ""
+
+#: includes/Plugin.php:394
+msgid "Getting Started"
+msgstr ""
+
+#: includes/Plugin.php:396
+msgid "Add your artists or bands."
+msgstr ""
+
+#: includes/Plugin.php:397
+msgid "Create albums and assign them to artists."
+msgstr ""
+
+#: includes/Plugin.php:398
+msgid "Upload tracks and add them to albums."
+msgstr ""
+
+#: includes/Plugin.php:399
+msgid "Create playlists to curate your music."
+msgstr ""
+
+#: includes/Plugin.php:400
+msgid "Share your music via ActivityPub to the Fediverse!"
+msgstr ""
+
+#: includes/Plugin.php:439
+msgid "License"
+msgstr ""
+
+#: includes/Plugin.php:440
+msgid "Default Settings"
+msgstr ""
+
+#: includes/Plugin.php:441
+msgid "Integrations"
+msgstr ""
+
+#: includes/Plugin.php:445
+msgid "FediStream Settings"
+msgstr ""
+
+#: includes/Plugin.php:493
+msgid "License settings saved."
+msgstr ""
+
+#: includes/Plugin.php:499
+msgid "Settings saved."
+msgstr ""
+
+#: includes/Plugin.php:505
+msgid "Integration settings saved."
+msgstr ""
+
+#: includes/Plugin.php:532
+msgid "License is active and valid."
+msgstr ""
+
+#: includes/Plugin.php:533
+msgid "License is invalid."
+msgstr ""
+
+#: includes/Plugin.php:534
+msgid "License has expired."
+msgstr ""
+
+#: includes/Plugin.php:535
+msgid "License has been revoked."
+msgstr ""
+
+#: includes/Plugin.php:536
+msgid "License is inactive. Please activate it."
+msgstr ""
+
+#: includes/Plugin.php:537
+msgid "License has not been validated yet."
+msgstr ""
+
+#: includes/Plugin.php:538
+msgid "License server is not configured."
+msgstr ""
+
+#: includes/Plugin.php:552
+msgid "Unknown status."
+msgstr ""
+
+#: includes/Plugin.php:565
+#, php-format
+msgid "Expires: %s"
+msgstr ""
+
+#: includes/Plugin.php:572
+msgid "Lifetime license"
+msgstr ""
+
+#: includes/Plugin.php:580
+#, php-format
+msgid "Last checked: %s"
+msgstr ""
+
+#: includes/Plugin.php:595
+msgid "License Server URL"
+msgstr ""
+
+#: includes/Plugin.php:601
+msgid "The URL of your license server."
+msgstr ""
+
+#: includes/Plugin.php:606
+msgid "License Key"
+msgstr ""
+
+#: includes/Plugin.php:612
+msgid "Your license key from your purchase."
+msgstr ""
+
+#: includes/Plugin.php:617
+msgid "Server Secret"
+msgstr ""
+
+#: includes/Plugin.php:622
+msgid ""
+"64-character verification secret from your license account. Leave empty to "
+"keep existing."
+msgstr ""
+
+#: includes/Plugin.php:628
+msgid "Save License Settings"
+msgstr ""
+
+#: includes/Plugin.php:631
+msgid "Validate License"
+msgstr ""
+
+#: includes/Plugin.php:635
+msgid "Activate License"
+msgstr ""
+
+#: includes/Plugin.php:664
+msgid "Max Upload Size"
+msgstr ""
+
+#: includes/Plugin.php:668
+msgid "Maximum file size for audio uploads."
+msgstr ""
+
+#: includes/Plugin.php:673
+msgid "Default License"
+msgstr ""
+
+#: includes/Plugin.php:677
+msgid "All Rights Reserved"
+msgstr ""
+
+#: includes/Plugin.php:684
+msgid "Default license for new uploads."
+msgstr ""
+
+#: includes/Plugin.php:708
+msgid "ActivityPub Integration"
+msgstr ""
+
+#: includes/Plugin.php:712
+msgid "Enable ActivityPub features"
+msgstr ""
+
+#: includes/Plugin.php:714
+msgid "Publish releases to the Fediverse and allow followers."
+msgstr ""
+
+#: includes/Plugin.php:718
+msgid "The ActivityPub plugin is recommended for full Fediverse integration."
+msgstr ""
+
+#: includes/Plugin.php:724
+msgid "WooCommerce Integration"
+msgstr ""
+
+#: includes/Plugin.php:728
+msgid "Enable WooCommerce features"
+msgstr ""
+
+#: includes/Plugin.php:733
+msgid "WooCommerce is not installed or active."
+msgstr ""
+
+#: includes/Plugin.php:736
+msgid "Sell albums and tracks through WooCommerce."
+msgstr ""
+
+#: includes/License/Manager.php:144 includes/License/Manager.php:244
+#: includes/License/Manager.php:325
+msgid "License server configuration is incomplete."
+msgstr ""
+
+#: includes/License/Manager.php:152 includes/License/Manager.php:252
+msgid "No license key provided."
+msgstr ""
+
+#: includes/License/Manager.php:170
+msgid "License validated successfully."
+msgstr ""
+
+#: includes/License/Manager.php:182 includes/License/Manager.php:278
+msgid "License key not found. Please check your license key."
+msgstr ""
+
+#: includes/License/Manager.php:188 includes/License/Manager.php:283
+msgid "Your license has expired. Please renew to continue."
+msgstr ""
+
+#: includes/License/Manager.php:194
+msgid "Your license has been revoked."
+msgstr ""
+
+#: includes/License/Manager.php:200
+msgid "License is inactive. Please activate it first."
+msgstr ""
+
+#: includes/License/Manager.php:206
+msgid "This license is not activated for this domain."
+msgstr ""
+
+#: includes/License/Manager.php:211 includes/License/Manager.php:288
+#: includes/License/Manager.php:395
+msgid "License verification failed. Please check your server secret."
+msgstr ""
+
+#: includes/License/Manager.php:216
+msgid "Too many requests. Please try again later."
+msgstr ""
+
+#: includes/License/Manager.php:223
+#, php-format
+msgid "License validation failed: %s"
+msgstr ""
+
+#: includes/License/Manager.php:230
+msgid "Unable to verify license. Please try again later."
+msgstr ""
+
+#: includes/License/Manager.php:273
+msgid ""
+"Maximum number of activations reached. Please deactivate another site first."
+msgstr ""
+
+#: includes/License/Manager.php:295
+#, php-format
+msgid "License activation failed: %s"
+msgstr ""
+
+#: includes/License/Manager.php:302
+msgid "Unable to activate license. Please try again later."
+msgstr ""
+
+#: includes/License/Manager.php:336
+msgid "No license key configured."
+msgstr ""
+
+#: includes/License/Manager.php:369 includes/License/Manager.php:379
+msgid "License is active."
+msgstr ""
+
+#: includes/License/Manager.php:370 includes/License/Manager.php:380
+msgid "License is not active."
+msgstr ""
+
+#: includes/License/Manager.php:387
+msgid "License key not found."
+msgstr ""
+
+#: includes/License/Manager.php:403
+msgid "Unable to check license status."
+msgstr ""
+
+#: includes/License/Manager.php:572 includes/License/Manager.php:595
+#: includes/License/Manager.php:618 includes/License/Manager.php:640
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: includes/License/Manager.php:626
+msgid "License deactivated."
+msgstr ""
+
+#: wp-fedistream.php:112
+#, php-format
+msgid ""
+"WP FediStream requires PHP version %1$s or higher. You are running PHP %2$s."
msgstr ""
#: wp-fedistream.php:127
+#, php-format
+msgid ""
+"WP FediStream requires WordPress version %1$s or higher. You are running "
+"WordPress %2$s."
+msgstr ""
+
+#: wp-fedistream.php:140
+msgid ""
+"WP FediStream requires Composer dependencies to be installed. Please run "
+"\"composer install\" in the plugin directory."
+msgstr ""
+
+#: wp-fedistream.php:156
+#, php-format
msgid "WP FediStream requires PHP version %s or higher."
msgstr ""
diff --git a/wp-fedistream.php b/wp-fedistream.php
index 0936497..bac8ed1 100644
--- a/wp-fedistream.php
+++ b/wp-fedistream.php
@@ -3,7 +3,7 @@
* Plugin Name: WP FediStream
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-fedistream
* Description: Stream music over ActivityPub - Build your own music streaming platform for Musicians and Labels.
- * Version: 0.2.0
+ * Version: 0.3.0
* Requires at least: 6.4
* Requires PHP: 8.3
* Author: Marco Graetsch
@@ -26,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
*
* @var string
*/
-define( 'WP_FEDISTREAM_VERSION', '0.2.0' );
+define( 'WP_FEDISTREAM_VERSION', '0.3.0' );
/**
* Plugin file path.