You've already forked wp-fedistream
feat: Initial release v0.1.0
WP FediStream - Stream music over ActivityPub Features: - Custom post types: Artist, Album, Track, Playlist - Custom taxonomies: Genre, Mood, License - User roles: Artist, Label - Admin dashboard with statistics - Frontend templates and shortcodes - Audio player with queue management - ActivityPub integration with actor support - WooCommerce product types for albums/tracks - User library with favorites and history - Notification system (in-app and email) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
15
.editorconfig
Normal file
15
.editorconfig
Normal file
@@ -0,0 +1,15 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
indent_size = 2
|
||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# For development purposes
|
||||
# Linked wordpress core and plugin folder
|
||||
wp-plugins
|
||||
wp-core
|
||||
vendor/
|
||||
releases/*
|
||||
143
CHANGELOG.md
Normal file
143
CHANGELOG.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to WP FediStream will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2026-01-28
|
||||
|
||||
Initial release of WP FediStream - a WordPress plugin for streaming music over ActivityPub.
|
||||
|
||||
### Added
|
||||
|
||||
#### Core Plugin Structure
|
||||
|
||||
- Plugin structure with WordPress Plugin API
|
||||
- Composer setup with Twig 3.0 template engine
|
||||
- Internationalization support with .pot template and German (de_CH) translation
|
||||
|
||||
#### Custom Post Types
|
||||
|
||||
- `fedistream_artist` - Artist/band profiles with social links, biography, and member management
|
||||
- `fedistream_album` - Albums, EPs, singles, and compilations with release metadata
|
||||
- `fedistream_track` - Individual tracks with audio upload, duration, BPM, key, and ISRC codes
|
||||
- `fedistream_playlist` - User-created playlists with drag-drop ordering
|
||||
|
||||
#### Custom Taxonomies
|
||||
|
||||
- `fedistream_genre` - Hierarchical music genres with default terms
|
||||
- `fedistream_mood` - Non-hierarchical mood tags for tracks and playlists
|
||||
- `fedistream_license` - Copyright and Creative Commons license options
|
||||
|
||||
#### User Roles
|
||||
|
||||
- `fedistream_artist` - Manage own content, upload files, view stats
|
||||
- `fedistream_label` - Manage all content, taxonomies, and view all statistics
|
||||
|
||||
#### Admin Interface
|
||||
|
||||
- Dashboard with statistics and quick actions
|
||||
- Organized menu under "FediStream"
|
||||
- Meta boxes for all post types with full metadata support
|
||||
- Settings page for ActivityPub and WooCommerce configuration
|
||||
- Custom list table columns with sortable fields
|
||||
- Artwork thumbnails, artist links, and duration display
|
||||
|
||||
#### Frontend Display
|
||||
|
||||
- Archive templates for all post types and taxonomies
|
||||
- Single templates with full metadata display
|
||||
- Card partials for responsive grid layouts
|
||||
- Comprehensive CSS styling with custom properties for theming
|
||||
|
||||
#### Shortcodes
|
||||
|
||||
- `[fedistream_artist]` - Display artist profile
|
||||
- `[fedistream_album]` - Display album with tracklist
|
||||
- `[fedistream_track]` - Display track with player
|
||||
- `[fedistream_playlist]` - Display playlist with tracks
|
||||
- `[fedistream_latest_releases]` - Recent releases grid
|
||||
- `[fedistream_popular_tracks]` - Popular tracks list
|
||||
- `[fedistream_artists]` - Artists grid
|
||||
- `[fedistream_player]` - Audio player widget
|
||||
- `[fedistream_library]` - User library page
|
||||
|
||||
#### Widgets
|
||||
|
||||
- Recent Releases Widget
|
||||
- Popular Tracks Widget
|
||||
- Featured Artist Widget
|
||||
- Now Playing Widget
|
||||
|
||||
#### Audio Player
|
||||
|
||||
- Full playback controls (play, pause, next, previous)
|
||||
- Queue management with add, clear, shuffle functionality
|
||||
- Repeat modes (none, all, one)
|
||||
- Shuffle mode with Fisher-Yates algorithm
|
||||
- Progress bar with seek functionality
|
||||
- Volume control with mute toggle and localStorage persistence
|
||||
- Media Session API integration for system controls
|
||||
- Play count tracking via AJAX
|
||||
|
||||
#### ActivityPub Integration
|
||||
|
||||
- Integration with WordPress ActivityPub plugin
|
||||
- Artists represented as Person/Group actors
|
||||
- RSA key generation for HTTP Signatures
|
||||
- Webfinger support for artist discovery
|
||||
- Object transformers for tracks, albums, and playlists
|
||||
- Inbox handling for Follow, Like, Announce, and Create activities
|
||||
- Outbox publishing with Create, Update, and Delete activities
|
||||
- Follower management with shared inbox deduplication
|
||||
- REST API endpoints for actors, inbox, outbox, and collections
|
||||
|
||||
#### WooCommerce Integration (Optional)
|
||||
|
||||
- Album product type extending WC_Product
|
||||
- Track product type extending WC_Product
|
||||
- Multiple download formats (MP3, FLAC, WAV, AAC, OGG)
|
||||
- Pricing models: Fixed, Pay What You Want, Name Your Price
|
||||
- Secure digital delivery with purchase verification
|
||||
- Album ZIP downloads with all tracks and cover art
|
||||
- Streaming access control based on purchases
|
||||
- 30-second preview for non-purchasers
|
||||
- Purchase tracking database table
|
||||
|
||||
#### User Library
|
||||
|
||||
- Favorite tracks, albums, and playlists
|
||||
- Follow local artists
|
||||
- Listening history tracking with clear option
|
||||
- Library page with tabs (Favorites, Artists, History)
|
||||
- Filter favorites by content type
|
||||
- AJAX endpoints for all library operations
|
||||
|
||||
#### Notification System
|
||||
|
||||
- In-app notifications with admin bar indicator
|
||||
- Email notifications with HTML templates
|
||||
- Notification types: new_release, new_follower, fediverse_like, fediverse_boost, playlist_added, purchase, system
|
||||
- Event triggers for releases, follows, and Fediverse interactions
|
||||
- User preference settings for email notifications
|
||||
- Real-time polling and mark as read functionality
|
||||
|
||||
#### Database Tables
|
||||
|
||||
- `fedistream_plays` - Track play statistics
|
||||
- `fedistream_playlist_tracks` - Playlist-track relationships
|
||||
- `fedistream_followers` - ActivityPub followers
|
||||
- `fedistream_purchases` - WooCommerce purchase tracking
|
||||
- `fedistream_favorites` - User favorites
|
||||
- `fedistream_user_follows` - Local artist follows
|
||||
- `fedistream_listening_history` - Track play history
|
||||
- `fedistream_notifications` - User notifications
|
||||
- `fedistream_reactions` - Fediverse reactions (likes, boosts)
|
||||
|
||||
---
|
||||
|
||||
[Unreleased]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/compare/v0.1.0...HEAD
|
||||
[0.1.0]: https://src.bundespruefstelle.ch/magdev/wp-fedistream/releases/tag/v0.1.0
|
||||
355
CLAUDE.md
Normal file
355
CLAUDE.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Wordpress Plugin to stream music over Activity Pub
|
||||
|
||||
**Author:** Marco Graetsch
|
||||
**Author URL:** <https://src.bundespruefstelle.ch/magdev>
|
||||
**Author Email:** <magdev3.0@gmail.com>
|
||||
**Repository URL:** <https://src.bundespruefstelle.ch/magdev/wp-fedistream>
|
||||
**Issues URL:** <https://src.bundespruefstelle.ch/magdev/wp-fedistream/issues>
|
||||
|
||||
## Project Overview
|
||||
|
||||
This plugin provides a way for Musicians or Music-Labels to cut the ties with Spotify, Youtube Music et al and build their own Streaming-Platform (optionally including selling their work using WooCommerce) base on the ActivityPub protocol. This plugin implements the management of Musicians (Single or Bands), Albums (or Releases), Tracks and Playlists.
|
||||
|
||||
The plugin utilizes the ActivityPub protocol to publish Releases or Tracks, share Playlists and let users create Playlist from different Wordpress instances, which use this plugin. It also employs Fediverse reactions to build comprehensive profile for Musicians and Bands (or the Label at all), including classicla Blogposts from Musicians. The Plugin should serve as a full Fediverse profile for Musicians. Everyone in the Fediverse is able to subscribe to the Label's or Musician's account.
|
||||
|
||||
If WooCommerce is installed, the Musician (or the Label) is allowed to sell the Music either as Album or single track utilizing special WooCommerce product types.
|
||||
|
||||
The goal is to create an alternative to the big tech streaming plaforms and give the musicians their freedom back and concentrate to make music. The first goal is to publish, the second goal is to monetize the platform without ripping of the users or musicians.
|
||||
|
||||
### Key Fact: 100% AI-Generated
|
||||
|
||||
This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase was created through AI assistance.
|
||||
|
||||
## Temporary Roadmap
|
||||
|
||||
**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.1.0
|
||||
|
||||
- Document all relevant implementation details in CLAUDE.md, drop PLAN.md, because we don't need it anymore. All relevent Infos are kept in CLAUDE.md from now on
|
||||
- Drop the current versioning, as this will be version 0.1.0. Merge the history contents in CHANGELOG.md into as single version 0.1.0
|
||||
- Update the README.md according to the last changes and current implementation
|
||||
- Commit the current sources to dev, merge it to main, tag it as 0.1.0 and push it all to origin
|
||||
- Cleanup this Version entry from the temporary raodmap and create to empty sections for the next bugfix-version and the minor version.
|
||||
- Call `/end-session`
|
||||
|
||||
## Technical Stack
|
||||
|
||||
- **Language:** PHP 8.3.x
|
||||
- **Framework:** Latest WordPress Plugin API
|
||||
- **E-commerce (optional):** WooCommerce 10.0+
|
||||
- **Template Engine:** Twig 3.0 (via Composer)
|
||||
- **Communication Protocol:** ActivityPub
|
||||
- **Wordpress Base Theme** twentytwentyfive
|
||||
- **Frontend:** Vanilla JavaScript
|
||||
- **Styling:** Custom CSS
|
||||
- **Dependency Management:** Composer
|
||||
- **Internationalization:** WordPress i18n (.pot/.po/.mo files)
|
||||
- **Canonical Plugin Name:** `wp-fedistream`
|
||||
|
||||
### Security Best Practices
|
||||
|
||||
- All user inputs are sanitized (integers for quantities/prices)
|
||||
- Nonce verification on form submissions
|
||||
- Output escaping in templates (`esc_attr`, `esc_html`, `esc_js`)
|
||||
- Direct file access prevention via `ABSPATH` check
|
||||
- XSS-safe DOM construction in JavaScript (no `innerHTML` with user data)
|
||||
- SQL injection prevention using `$wpdb->prepare()` throughout
|
||||
|
||||
### Translation Ready
|
||||
|
||||
All user-facing strings use:
|
||||
|
||||
```php
|
||||
__('Text to translate', 'wp-fedistream')
|
||||
_e('Text to translate', 'wp-fedistream')
|
||||
```
|
||||
|
||||
Text domain: `wp-fedistream`
|
||||
|
||||
#### Translation Template
|
||||
|
||||
- Base `.pot` file created: `languages/wp-fedistream.pot`
|
||||
- Ready for translation to any locale
|
||||
- All translatable strings properly marked with text domain
|
||||
|
||||
#### Available Translations
|
||||
|
||||
- `en_US` - English (United States) [base language - .pot template]
|
||||
- `de_CH` - German (Switzerland, formal)
|
||||
|
||||
To compile translations to .mo files for production:
|
||||
|
||||
```bash
|
||||
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||
```
|
||||
|
||||
### Create releases
|
||||
|
||||
- The `vendor/` directory MUST be included in releases (Dependencies required for runtime)
|
||||
- **Don't create any release files until version 0.1.x and up!**
|
||||
- **CRITICAL**: Build `vendor/` for the MINIMUM supported PHP version, not the development version
|
||||
- Use `composer config platform.php 8.3.0` before building release packages
|
||||
- Run `composer update --no-dev --optimize-autoloader` to rebuild dependencies
|
||||
- **CRITICAL**: WordPress requires plugins in a subdirectory structure
|
||||
- Run zip from the `plugins/` parent directory, NOT from within the plugin directory
|
||||
- Package must extract to `wp-fedistream/` subdirectory with main file at `wp-fedistream/wp-fedistream.php`
|
||||
- Correct command: `cd /wp-content/plugins/ && zip -r wp-fedistream/releases/wp-fedistream-x.x.x.zip wp-fedistream ...`
|
||||
- Wrong: Running zip from inside the plugin directory creates files at root level
|
||||
- **CRITICAL**: Exclude symlinks explicitly - zip follows symlinks by default
|
||||
- Always use `-x "wp-fedistream/wp-core" -x "wp-fedistream/wp-core/*" -x "wp-fedistream/wp-plugins" -x "wp-fedistream/wp-plugins/*"` to exclude development symlinks
|
||||
- Otherwise the entire linked directory contents will be included in the package
|
||||
- Exclusion patterns must match the relative path structure used in zip command
|
||||
- Always verify the package structure with `unzip -l` before distribution
|
||||
- Check all files are prefixed with `wp-fedistream/`
|
||||
- Verify main file is at `wp-fedistream/wp-fedistream.php`
|
||||
- Check for duplicate entries (indicates multiple builds in same archive)
|
||||
- Test installation on the minimum supported PHP version before final deployment
|
||||
- Releases are stored in `releases/` including checksums
|
||||
- Track release changes in a single `CHANGELOG.md` file
|
||||
- Bump the version number to either bugfix release versions or on new features minor release versions
|
||||
- **CRITICAL**: WordPress reads version from TWO places - BOTH must be updated:
|
||||
1. Plugin header comment `Version: x.x.x` (line ~6 in wc-licensed-product.php) - WordPress uses THIS for admin display
|
||||
2. PHP constant `WP_FEDISTREAM_VERSION` (line ~28) - Used internally by the plugin
|
||||
- If only the constant is updated, WordPress will show the old version in Plugins list
|
||||
|
||||
**Important Git Notes:**
|
||||
|
||||
- Default branch while development is `dev`
|
||||
- Create releases from branch `main` after merging branch `dev`
|
||||
- Tags should use format `vX.X.X` (e.g., `v1.1.22`), start with v0.1.0
|
||||
- Use annotated tags (`-a`) not lightweight tags
|
||||
- Commit messages should follow the established format with Claude Code attribution
|
||||
- `.claude/settings.local.json` changes are typically local-only (stash before rebasing)
|
||||
|
||||
#### What Gets Released
|
||||
|
||||
- All plugin source files
|
||||
- Compiled vendor dependencies
|
||||
- Translation files (.mo compiled from .po)
|
||||
- Assets (CSS, JS)
|
||||
- Documentation (README, CHANGELOG, etc.)
|
||||
|
||||
#### What's Excluded
|
||||
|
||||
- Git metadata (`.git/`)
|
||||
- Development files (`.vscode/`, `.claude/`, `CLAUDE.md`, `wp-core`, `wp-plugins`)
|
||||
- Logs and cache files
|
||||
- Previous releases
|
||||
- `composer.lock` (but `vendor/` is included)
|
||||
|
||||
---
|
||||
|
||||
**For AI Assistants:**
|
||||
|
||||
When starting a new session on this project:
|
||||
|
||||
1. Read this CLAUDE.md file first
|
||||
2. Semantic versioning follows the `MAJOR.MINOR.BUGFIX` pattern
|
||||
3. Check git log for recent changes
|
||||
4. Verify you're on the `dev` branch before making changes
|
||||
5. Run `composer install` if vendor/ is missing
|
||||
6. Test changes before committing
|
||||
7. Follow commit message format with Claude Code attribution
|
||||
8. Update this session history section with learnings
|
||||
9. Never commit backup files (`*.po~`, `*.bak`, etc.) - check `git status` before committing
|
||||
10. Follow markdown linting rules (see below)
|
||||
|
||||
Always refer to this document when starting work on this project.
|
||||
|
||||
### Markdown Linting Rules
|
||||
|
||||
When editing CLAUDE.md or other markdown files, follow these rules to avoid linting errors:
|
||||
|
||||
1. **MD031 - Blank lines around fenced code blocks**: Always add a blank line before and after fenced code blocks, even when they follow list items. Example of correct format:
|
||||
|
||||
- **Item label**:
|
||||
|
||||
(blank line here)
|
||||
\`\`\`php
|
||||
code example
|
||||
\`\`\`
|
||||
(blank line here)
|
||||
|
||||
2. **MD056 - Table column count**: Table separators must have matching column counts and proper spacing. Use consistent dash lengths that match column header widths.
|
||||
|
||||
3. **MD009 - No trailing spaces**: Remove trailing whitespace from lines
|
||||
|
||||
4. **MD012 - No multiple consecutive blank lines**: Use only single blank lines between sections
|
||||
|
||||
5. **MD040 - Fenced code blocks should have a language specified**: Always add a language identifier to code blocks (e.g., `txt`, `bash`, `php`). For shortcode examples, use `txt`.
|
||||
|
||||
6. **MD032 - Lists should be surrounded by blank lines**: Add a blank line before AND after list blocks, including after bold labels like `**Attributes:**`.
|
||||
|
||||
7. **MD034 - Bare URLs**: Wrap URLs in angle brackets (e.g., `<https://example.com>`) or use markdown link syntax `[text](url)`.
|
||||
|
||||
8. **Author section formatting**: Use a heading (`### Name`) instead of bold (`**Name**`) for the author name to maintain consistent document structure.
|
||||
|
||||
## Project Architecture
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```txt
|
||||
wp-fedistream/
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ ├── admin.css # Admin interface styles
|
||||
│ │ └── frontend.css # Frontend styles
|
||||
│ ├── js/
|
||||
│ │ ├── admin.js # Admin interface scripts
|
||||
│ │ ├── frontend.js # Frontend scripts
|
||||
│ │ ├── player.js # Audio player
|
||||
│ │ ├── library.js # User library page
|
||||
│ │ └── notifications.js # Notification system
|
||||
│ └── images/
|
||||
├── includes/
|
||||
│ ├── ActivityPub/
|
||||
│ │ ├── Integration.php # ActivityPub plugin integration
|
||||
│ │ ├── ArtistActor.php # Artist as ActivityPub actor
|
||||
│ │ ├── Inbox.php # Process incoming activities
|
||||
│ │ ├── Outbox.php # Publish outgoing activities
|
||||
│ │ ├── RestApi.php # REST endpoints for ActivityPub
|
||||
│ │ └── Transformers/ # Object transformers
|
||||
│ │ ├── TrackTransformer.php
|
||||
│ │ ├── AlbumTransformer.php
|
||||
│ │ └── PlaylistTransformer.php
|
||||
│ ├── Admin/
|
||||
│ │ └── ListColumns.php # Custom list table columns
|
||||
│ ├── Frontend/
|
||||
│ │ ├── Ajax.php # AJAX handlers
|
||||
│ │ ├── Shortcodes.php # All shortcodes
|
||||
│ │ ├── TemplateLoader.php # Template loading
|
||||
│ │ └── Widgets.php # Widget registration
|
||||
│ ├── PostTypes/
|
||||
│ │ ├── Artist.php # fedistream_artist
|
||||
│ │ ├── Album.php # fedistream_album
|
||||
│ │ ├── Track.php # fedistream_track
|
||||
│ │ └── Playlist.php # fedistream_playlist
|
||||
│ ├── Roles/
|
||||
│ │ └── Capabilities.php # User roles and caps
|
||||
│ ├── Taxonomies/
|
||||
│ │ ├── Genre.php # fedistream_genre
|
||||
│ │ ├── Mood.php # fedistream_mood
|
||||
│ │ └── License.php # fedistream_license
|
||||
│ ├── User/
|
||||
│ │ ├── Library.php # Favorites, follows, history
|
||||
│ │ ├── LibraryPage.php # Library shortcode
|
||||
│ │ └── Notifications.php # Notification system
|
||||
│ ├── WooCommerce/
|
||||
│ │ ├── Integration.php # WooCommerce setup
|
||||
│ │ ├── AlbumProduct.php # Album product type
|
||||
│ │ ├── TrackProduct.php # Track product type
|
||||
│ │ ├── DigitalDelivery.php # Download handling
|
||||
│ │ └── StreamingAccess.php # Access control
|
||||
│ ├── Installer.php # Database setup, activation
|
||||
│ └── Plugin.php # Main singleton class
|
||||
├── languages/
|
||||
│ ├── wp-fedistream.pot # Translation template
|
||||
│ └── wp-fedistream-de_CH.po # German (Switzerland)
|
||||
├── templates/ # Twig templates
|
||||
│ ├── admin/
|
||||
│ ├── archive/
|
||||
│ ├── single/
|
||||
│ └── player/
|
||||
├── vendor/ # Composer dependencies
|
||||
├── composer.json
|
||||
├── uninstall.php
|
||||
└── wp-fedistream.php # Plugin entry point
|
||||
```
|
||||
|
||||
### Database Tables
|
||||
|
||||
| Table | Purpose |
|
||||
| ----- | ------- |
|
||||
| `{prefix}_fedistream_plays` | Track play statistics |
|
||||
| `{prefix}_fedistream_playlist_tracks` | Playlist-track relationships |
|
||||
| `{prefix}_fedistream_followers` | ActivityPub followers |
|
||||
| `{prefix}_fedistream_purchases` | WooCommerce purchase tracking |
|
||||
| `{prefix}_fedistream_favorites` | User favorites |
|
||||
| `{prefix}_fedistream_user_follows` | Local artist follows |
|
||||
| `{prefix}_fedistream_listening_history` | Track play history |
|
||||
| `{prefix}_fedistream_notifications` | User notifications |
|
||||
| `{prefix}_fedistream_reactions` | Fediverse reactions |
|
||||
|
||||
### Custom Post Types
|
||||
|
||||
| Post Type | Slug | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `fedistream_artist` | `/artists/` | Musicians, bands, collectives |
|
||||
| `fedistream_album` | `/albums/` | Albums, EPs, singles, compilations |
|
||||
| `fedistream_track` | `/tracks/` | Individual audio tracks |
|
||||
| `fedistream_playlist` | `/playlists/` | Curated track collections |
|
||||
|
||||
### Custom Taxonomies
|
||||
|
||||
| Taxonomy | Type | Applied To |
|
||||
| -------- | ---- | ---------- |
|
||||
| `fedistream_genre` | Hierarchical | Artists, Albums, Tracks |
|
||||
| `fedistream_mood` | Non-hierarchical | Tracks, Playlists |
|
||||
| `fedistream_license` | Hierarchical | Albums, Tracks |
|
||||
|
||||
### User Roles
|
||||
|
||||
| Role | Slug | Capabilities |
|
||||
| ---- | ---- | ------------ |
|
||||
| Artist | `fedistream_artist` | Manage own content, upload files |
|
||||
| Label | `fedistream_label` | Manage all content, taxonomies, stats |
|
||||
|
||||
### Shortcodes
|
||||
|
||||
| Shortcode | Description |
|
||||
| --------- | ----------- |
|
||||
| `[fedistream_player track_id="123"]` | Single track player |
|
||||
| `[fedistream_playlist id="456"]` | Playlist display |
|
||||
| `[fedistream_album id="789"]` | Album display |
|
||||
| `[fedistream_artist id="101"]` | Artist profile |
|
||||
| `[fedistream_recent_releases count="5"]` | Recent releases |
|
||||
| `[fedistream_popular_tracks count="10"]` | Popular tracks |
|
||||
| `[fedistream_library]` | User library page |
|
||||
|
||||
### Key Classes
|
||||
|
||||
- `Plugin` (singleton) - Main controller, initializes all components
|
||||
- `Installer` - Database setup, activation/deactivation hooks
|
||||
- `Artist`, `Album`, `Track`, `Playlist` - Post type registration and meta boxes
|
||||
- `Genre`, `Mood`, `License` - Taxonomy registration with defaults
|
||||
- `Capabilities` - User role and capability management
|
||||
- `ActivityPubIntegration` - Integration with WordPress ActivityPub plugin
|
||||
- `ArtistActor` - Artist profiles as ActivityPub Person/Group actors
|
||||
- `Inbox` - Process Follow, Like, Announce, Create activities
|
||||
- `Outbox` - Publish Create, Update activities to followers
|
||||
- `WooCommerceIntegration` - Custom product types for albums/tracks
|
||||
- `DigitalDelivery` - Secure download handling with ZIP support
|
||||
- `StreamingAccess` - Purchase-based streaming control
|
||||
- `Library` - User favorites, follows, listening history
|
||||
- `Notifications` - In-app and email notification system
|
||||
|
||||
### REST API Endpoints
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
| -------- | ------ | ------- |
|
||||
| `/wp-json/fedistream/v1/artists/{id}/actor` | GET | ActivityPub actor profile |
|
||||
| `/wp-json/fedistream/v1/artists/{id}/inbox` | POST | ActivityPub inbox |
|
||||
| `/wp-json/fedistream/v1/artists/{id}/outbox` | GET | ActivityPub outbox |
|
||||
| `/wp-json/fedistream/v1/artists/{id}/followers` | GET | Followers collection |
|
||||
| `/wp-json/fedistream/v1/artists/{id}/following` | GET | Following collection |
|
||||
|
||||
### AJAX Actions
|
||||
|
||||
| Action | Purpose |
|
||||
| ------ | ------- |
|
||||
| `fedistream_record_play` | Record track play |
|
||||
| `fedistream_toggle_favorite` | Add/remove favorite |
|
||||
| `fedistream_toggle_follow` | Follow/unfollow artist |
|
||||
| `fedistream_get_library` | Get user library |
|
||||
| `fedistream_get_followed_artists` | Get followed artists |
|
||||
| `fedistream_get_history` | Get listening history |
|
||||
| `fedistream_clear_history` | Clear listening history |
|
||||
| `fedistream_get_notifications` | Get user notifications |
|
||||
| `fedistream_mark_notification_read` | Mark notification read |
|
||||
| `fedistream_mark_all_notifications_read` | Mark all read |
|
||||
| `fedistream_delete_notification` | Delete notification |
|
||||
|
||||
---
|
||||
|
||||
## Session History
|
||||
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# WP FediStream
|
||||
|
||||
Stream music over ActivityPub - Build your own music streaming platform for Musicians and Labels.
|
||||
|
||||
[](CHANGELOG.md)
|
||||
[](https://php.net)
|
||||
[](https://wordpress.org)
|
||||
[](https://www.gnu.org/licenses/gpl-2.0.html)
|
||||
|
||||
## Description
|
||||
|
||||
WP FediStream is a WordPress plugin that enables musicians, bands, and labels to create their own music streaming platform. It leverages the ActivityPub protocol to connect with the Fediverse, allowing artists to share their music with followers across Mastodon, Pixelfed, and other federated platforms.
|
||||
|
||||
**This project is proudly "vibe-coded" using Claude AI** - the entire codebase was created through AI assistance.
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Artist Management** - Create profiles for solo artists, bands, duos, and collectives
|
||||
- **Album/Release Management** - Organize music into albums, EPs, singles, and compilations
|
||||
- **Track Management** - Upload and manage individual tracks with full metadata support
|
||||
- **Playlist Creation** - Curate playlists with drag-and-drop track ordering
|
||||
- **Audio Player** - Full-featured player with queue, shuffle, repeat, and volume controls
|
||||
- **Genre & Mood Taxonomies** - Organize music by genre and mood for easy discovery
|
||||
- **Licensing Options** - Support for Creative Commons and traditional copyright
|
||||
- **ActivityPub Integration** - Share releases to the Fediverse with full actor support
|
||||
- **WooCommerce Integration** - Sell music directly from your site with custom product types
|
||||
- **User Library** - Favorites, artist follows, and listening history
|
||||
- **Notifications** - In-app and email notifications for new releases and interactions
|
||||
|
||||
## Requirements
|
||||
|
||||
- PHP 8.3 or higher
|
||||
- WordPress 6.4 or higher
|
||||
- Composer (for development/installation)
|
||||
|
||||
### Optional
|
||||
|
||||
- [ActivityPub Plugin](https://wordpress.org/plugins/activitypub/) - For Fediverse integration
|
||||
- [WooCommerce](https://woocommerce.com/) 10.0+ - For selling music
|
||||
|
||||
## Installation
|
||||
|
||||
### From Source
|
||||
|
||||
1. Clone or download the repository to your WordPress plugins directory:
|
||||
|
||||
```bash
|
||||
cd wp-content/plugins/
|
||||
git clone https://src.bundespruefstelle.ch/magdev/wp-fedistream.git
|
||||
```
|
||||
|
||||
2. Install Composer dependencies:
|
||||
|
||||
```bash
|
||||
cd wp-fedistream
|
||||
composer install --no-dev
|
||||
```
|
||||
|
||||
3. Activate the plugin in WordPress admin under **Plugins > Installed Plugins**
|
||||
|
||||
4. Navigate to **FediStream** in the admin menu to get started
|
||||
|
||||
## Usage
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. **Add Artists** - Create profiles for your artists or bands
|
||||
2. **Create Albums** - Set up albums and assign them to artists
|
||||
3. **Upload Tracks** - Add tracks to albums with audio files
|
||||
4. **Create Playlists** - Curate collections of tracks
|
||||
5. **Share via ActivityPub** - Publish releases to the Fediverse and gain followers
|
||||
|
||||
### Custom Post Types
|
||||
|
||||
| Post Type | Description | URL Slug |
|
||||
|-----------|-------------|----------|
|
||||
| Artist | Musicians, bands, collectives | `/artists/` |
|
||||
| Album | Albums, EPs, singles, compilations | `/albums/` |
|
||||
| Track | Individual audio tracks | `/tracks/` |
|
||||
| Playlist | Curated track collections | `/playlists/` |
|
||||
|
||||
### Taxonomies
|
||||
|
||||
| Taxonomy | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| Genre | Hierarchical | Music genres (Rock > Indie Rock) |
|
||||
| Mood | Non-hierarchical | Track/playlist moods (Energetic, Calm) |
|
||||
| License | Hierarchical | Copyright and Creative Commons licenses |
|
||||
|
||||
### Shortcodes
|
||||
|
||||
| Shortcode | Description |
|
||||
|-----------|-------------|
|
||||
| `[fedistream_artist id="123"]` | Display artist profile |
|
||||
| `[fedistream_album id="456"]` | Display album with tracklist |
|
||||
| `[fedistream_track id="789"]` | Display track with player |
|
||||
| `[fedistream_playlist id="101"]` | Display playlist with tracks |
|
||||
| `[fedistream_latest_releases]` | Recent releases grid |
|
||||
| `[fedistream_popular_tracks]` | Popular tracks list |
|
||||
| `[fedistream_player]` | Audio player widget |
|
||||
| `[fedistream_library]` | User library page |
|
||||
|
||||
### User Roles
|
||||
|
||||
| Role | Capabilities |
|
||||
|------|--------------|
|
||||
| FediStream Artist | Manage own content, upload files, view stats |
|
||||
| FediStream Label | Manage all content, manage taxonomies, view all stats |
|
||||
|
||||
## File Structure
|
||||
|
||||
```txt
|
||||
wp-fedistream/
|
||||
├── assets/
|
||||
│ ├── css/ # Stylesheets
|
||||
│ ├── js/ # JavaScript
|
||||
│ └── images/ # Images
|
||||
├── includes/
|
||||
│ ├── ActivityPub/ # ActivityPub integration
|
||||
│ ├── Admin/ # Admin interface classes
|
||||
│ ├── Frontend/ # Frontend components
|
||||
│ ├── PostTypes/ # Custom post type classes
|
||||
│ ├── Taxonomies/ # Custom taxonomy classes
|
||||
│ ├── Roles/ # User roles and capabilities
|
||||
│ ├── User/ # User library and notifications
|
||||
│ ├── WooCommerce/ # WooCommerce integration
|
||||
│ ├── Plugin.php # Main plugin singleton
|
||||
│ └── Installer.php # Activation/deactivation
|
||||
├── languages/ # Translation files
|
||||
├── templates/ # Twig templates
|
||||
├── vendor/ # Composer dependencies
|
||||
├── CHANGELOG.md # Version history
|
||||
└── wp-fedistream.php # Plugin entry point
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
This project is in early development. Contributions, bug reports, and feature requests are welcome.
|
||||
|
||||
- **Repository:** <https://src.bundespruefstelle.ch/magdev/wp-fedistream>
|
||||
- **Issues:** <https://src.bundespruefstelle.ch/magdev/wp-fedistream/issues>
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GPL v2 or later.
|
||||
|
||||
## Author
|
||||
|
||||
**Marco Graetsch**
|
||||
|
||||
- Website: <https://src.bundespruefstelle.ch/magdev>
|
||||
- Email: <magdev3.0@gmail.com>
|
||||
|
||||
---
|
||||
|
||||
*Built with Claude AI*
|
||||
7
assets/css/admin.css
Normal file
7
assets/css/admin.css
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* WP FediStream - Admin Styles
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
/* Admin styles will be added here */
|
||||
1408
assets/css/frontend.css
Normal file
1408
assets/css/frontend.css
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/css/index.php
Normal file
1
assets/css/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
1
assets/images/index.php
Normal file
1
assets/images/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
1
assets/index.php
Normal file
1
assets/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
14
assets/js/admin.js
Normal file
14
assets/js/admin.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* WP FediStream - Admin Scripts
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function() {
|
||||
// Admin scripts will be added here
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
839
assets/js/frontend.js
Normal file
839
assets/js/frontend.js
Normal file
@@ -0,0 +1,839 @@
|
||||
/**
|
||||
* WP FediStream - Frontend Scripts
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* FediStream Audio Player
|
||||
* Handles audio playback, queue management, and UI updates
|
||||
*/
|
||||
class FediStreamPlayer {
|
||||
constructor() {
|
||||
this.audio = new Audio();
|
||||
this.queue = [];
|
||||
this.currentIndex = -1;
|
||||
this.isPlaying = false;
|
||||
this.isShuffle = false;
|
||||
this.repeatMode = 'none'; // none, all, one
|
||||
this.volume = 0.8;
|
||||
this.originalQueue = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the player
|
||||
*/
|
||||
init() {
|
||||
this.audio.volume = this.volume;
|
||||
this.bindAudioEvents();
|
||||
this.bindUIEvents();
|
||||
this.initNowPlayingWidgets();
|
||||
this.loadVolumeFromStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind audio element events
|
||||
*/
|
||||
bindAudioEvents() {
|
||||
this.audio.addEventListener('timeupdate', () => this.onTimeUpdate());
|
||||
this.audio.addEventListener('loadedmetadata', () => this.onLoadedMetadata());
|
||||
this.audio.addEventListener('ended', () => this.onEnded());
|
||||
this.audio.addEventListener('play', () => this.onPlay());
|
||||
this.audio.addEventListener('pause', () => this.onPause());
|
||||
this.audio.addEventListener('error', (e) => this.onError(e));
|
||||
this.audio.addEventListener('waiting', () => this.onWaiting());
|
||||
this.audio.addEventListener('canplay', () => this.onCanPlay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind UI event listeners
|
||||
*/
|
||||
bindUIEvents() {
|
||||
// Play buttons on cards and tracklists
|
||||
document.addEventListener('click', (e) => {
|
||||
const playBtn = e.target.closest('[data-track-id]');
|
||||
if (playBtn && (playBtn.classList.contains('fedistream-card__play-overlay') ||
|
||||
playBtn.classList.contains('fedistream-tracklist__play') ||
|
||||
playBtn.classList.contains('fedistream-widget__play') ||
|
||||
playBtn.classList.contains('fedistream-single__play-overlay') ||
|
||||
playBtn.classList.contains('fedistream-track__play-overlay'))) {
|
||||
e.preventDefault();
|
||||
const trackId = playBtn.dataset.trackId;
|
||||
this.playTrackById(trackId);
|
||||
}
|
||||
|
||||
// Tracklist item click (not on play button or link)
|
||||
const tracklistItem = e.target.closest('.fedistream-tracklist__item');
|
||||
if (tracklistItem && !e.target.closest('a') && !e.target.closest('button')) {
|
||||
e.preventDefault();
|
||||
const trackId = tracklistItem.dataset.trackId;
|
||||
if (trackId) {
|
||||
this.playTrackById(trackId);
|
||||
}
|
||||
}
|
||||
|
||||
// Play all button
|
||||
if (e.target.closest('.fedistream-btn--play-all')) {
|
||||
e.preventDefault();
|
||||
const btn = e.target.closest('.fedistream-btn--play-all');
|
||||
const albumId = btn.dataset.albumId;
|
||||
const playlistId = btn.dataset.playlistId;
|
||||
if (albumId) {
|
||||
this.playAlbum(albumId);
|
||||
} else if (playlistId) {
|
||||
this.playPlaylist(playlistId);
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle button
|
||||
if (e.target.closest('.fedistream-btn--shuffle')) {
|
||||
e.preventDefault();
|
||||
const btn = e.target.closest('.fedistream-btn--shuffle');
|
||||
const albumId = btn.dataset.albumId;
|
||||
const playlistId = btn.dataset.playlistId;
|
||||
if (albumId) {
|
||||
this.playAlbum(albumId, true);
|
||||
} else if (playlistId) {
|
||||
this.playPlaylist(playlistId, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Player controls
|
||||
document.addEventListener('click', (e) => {
|
||||
// Main play/pause button
|
||||
if (e.target.closest('.fedistream-player__btn--play')) {
|
||||
e.preventDefault();
|
||||
this.togglePlayPause();
|
||||
}
|
||||
|
||||
// Previous button
|
||||
if (e.target.closest('.fedistream-player__btn--prev')) {
|
||||
e.preventDefault();
|
||||
this.previous();
|
||||
}
|
||||
|
||||
// Next button
|
||||
if (e.target.closest('.fedistream-player__btn--next')) {
|
||||
e.preventDefault();
|
||||
this.next();
|
||||
}
|
||||
|
||||
// Shuffle button
|
||||
if (e.target.closest('.fedistream-player__btn--shuffle')) {
|
||||
e.preventDefault();
|
||||
this.toggleShuffle();
|
||||
e.target.closest('.fedistream-player__btn--shuffle').classList.toggle('is-active', this.isShuffle);
|
||||
}
|
||||
|
||||
// Repeat button
|
||||
if (e.target.closest('.fedistream-player__btn--repeat')) {
|
||||
e.preventDefault();
|
||||
this.toggleRepeat();
|
||||
const btn = e.target.closest('.fedistream-player__btn--repeat');
|
||||
btn.classList.remove('is-active', 'is-repeat-one');
|
||||
if (this.repeatMode === 'all') {
|
||||
btn.classList.add('is-active');
|
||||
} else if (this.repeatMode === 'one') {
|
||||
btn.classList.add('is-active', 'is-repeat-one');
|
||||
}
|
||||
}
|
||||
|
||||
// Volume button (mute toggle)
|
||||
if (e.target.closest('.fedistream-player__btn--volume')) {
|
||||
e.preventDefault();
|
||||
this.toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
// Seek slider
|
||||
document.addEventListener('input', (e) => {
|
||||
if (e.target.classList.contains('fedistream-player__seek')) {
|
||||
const value = e.target.value;
|
||||
const duration = this.audio.duration || 0;
|
||||
this.audio.currentTime = (value / 100) * duration;
|
||||
}
|
||||
|
||||
// Volume slider
|
||||
if (e.target.classList.contains('fedistream-player__volume-slider')) {
|
||||
const value = e.target.value / 100;
|
||||
this.setVolume(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Click on progress bar
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.classList.contains('fedistream-player__bar')) {
|
||||
const rect = e.target.getBoundingClientRect();
|
||||
const percent = (e.clientX - rect.left) / rect.width;
|
||||
const duration = this.audio.duration || 0;
|
||||
this.audio.currentTime = percent * duration;
|
||||
}
|
||||
});
|
||||
|
||||
// Queue item click (in multi-track player)
|
||||
document.addEventListener('click', (e) => {
|
||||
const queueItem = e.target.closest('.fedistream-tracklist--queue .fedistream-tracklist__item');
|
||||
if (queueItem && !e.target.closest('a')) {
|
||||
e.preventDefault();
|
||||
const index = parseInt(queueItem.dataset.trackIndex, 10);
|
||||
if (!isNaN(index)) {
|
||||
this.playAtIndex(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Now playing widget controls
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.fedistream-now-playing__btn--play')) {
|
||||
e.preventDefault();
|
||||
this.togglePlayPause();
|
||||
}
|
||||
if (e.target.closest('.fedistream-now-playing__btn--prev')) {
|
||||
e.preventDefault();
|
||||
this.previous();
|
||||
}
|
||||
if (e.target.closest('.fedistream-now-playing__btn--next')) {
|
||||
e.preventDefault();
|
||||
this.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize inline players
|
||||
*/
|
||||
initInlinePlayers() {
|
||||
const players = document.querySelectorAll('.fedistream-player[data-track-id][data-audio-url]');
|
||||
players.forEach(player => {
|
||||
const trackId = player.dataset.trackId;
|
||||
const audioUrl = player.dataset.audioUrl;
|
||||
|
||||
// Store reference for later
|
||||
player._fedistream = {
|
||||
trackId,
|
||||
audioUrl
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize multi-track players
|
||||
*/
|
||||
initMultiTrackPlayers() {
|
||||
const players = document.querySelectorAll('.fedistream-player--multi[data-tracks]');
|
||||
players.forEach(player => {
|
||||
try {
|
||||
const tracks = JSON.parse(player.dataset.tracks);
|
||||
player._fedistream = { tracks };
|
||||
} catch (e) {
|
||||
console.error('Failed to parse tracks data', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize now playing widgets
|
||||
*/
|
||||
initNowPlayingWidgets() {
|
||||
this.nowPlayingWidgets = document.querySelectorAll('[data-widget="now-playing"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a track by ID
|
||||
*/
|
||||
async playTrackById(trackId) {
|
||||
// Check if we have track data in the DOM
|
||||
const trackElement = document.querySelector(`[data-track-id="${trackId}"]`);
|
||||
let audioUrl = null;
|
||||
|
||||
// Try to get audio URL from inline player
|
||||
const inlinePlayer = document.querySelector(`.fedistream-player[data-track-id="${trackId}"]`);
|
||||
if (inlinePlayer && inlinePlayer.dataset.audioUrl) {
|
||||
audioUrl = inlinePlayer.dataset.audioUrl;
|
||||
}
|
||||
|
||||
// If no audio URL, try to fetch from API
|
||||
if (!audioUrl) {
|
||||
const trackData = await this.fetchTrackData(trackId);
|
||||
if (trackData && trackData.audio_url) {
|
||||
audioUrl = trackData.audio_url;
|
||||
}
|
||||
}
|
||||
|
||||
if (!audioUrl) {
|
||||
console.error('No audio URL found for track', trackId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build track object
|
||||
const track = {
|
||||
id: trackId,
|
||||
audio_url: audioUrl,
|
||||
title: this.getTrackTitle(trackId),
|
||||
artist: this.getTrackArtist(trackId),
|
||||
thumbnail: this.getTrackThumbnail(trackId)
|
||||
};
|
||||
|
||||
// Clear queue and play single track
|
||||
this.clearQueue();
|
||||
this.addToQueue(track);
|
||||
this.playAtIndex(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch track data from API
|
||||
*/
|
||||
async fetchTrackData(trackId) {
|
||||
if (!window.wpFediStream || !window.wpFediStream.ajaxUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'fedistream_get_track');
|
||||
formData.append('track_id', trackId);
|
||||
formData.append('nonce', window.wpFediStream.nonce);
|
||||
|
||||
const response = await fetch(window.wpFediStream.ajaxUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
return data.data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch track data', e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track title from DOM
|
||||
*/
|
||||
getTrackTitle(trackId) {
|
||||
const el = document.querySelector(`[data-track-id="${trackId}"] .fedistream-tracklist__title, [data-track-id="${trackId}"] .fedistream-card__title`);
|
||||
return el ? el.textContent.trim() : 'Unknown Track';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track artist from DOM
|
||||
*/
|
||||
getTrackArtist(trackId) {
|
||||
const el = document.querySelector(`[data-track-id="${trackId}"] .fedistream-tracklist__artist, [data-track-id="${trackId}"] .fedistream-card__artist`);
|
||||
return el ? el.textContent.trim() : 'Unknown Artist';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track thumbnail from DOM
|
||||
*/
|
||||
getTrackThumbnail(trackId) {
|
||||
const el = document.querySelector(`[data-track-id="${trackId}"] .fedistream-tracklist__artwork, [data-track-id="${trackId}"] .fedistream-card__image img`);
|
||||
return el ? el.src : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Play album
|
||||
*/
|
||||
async playAlbum(albumId, shuffle = false) {
|
||||
const tracklist = document.querySelector(`.fedistream-btn--play-all[data-album-id="${albumId}"]`)?.closest('.fedistream-single')?.querySelector('.fedistream-tracklist');
|
||||
|
||||
if (tracklist) {
|
||||
this.playTracklist(tracklist, shuffle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play playlist
|
||||
*/
|
||||
async playPlaylist(playlistId, shuffle = false) {
|
||||
const tracklist = document.querySelector(`.fedistream-btn--play-all[data-playlist-id="${playlistId}"]`)?.closest('.fedistream-single')?.querySelector('.fedistream-tracklist');
|
||||
|
||||
if (tracklist) {
|
||||
this.playTracklist(tracklist, shuffle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play all tracks from a tracklist element
|
||||
*/
|
||||
playTracklist(tracklistElement, shuffle = false) {
|
||||
const items = tracklistElement.querySelectorAll('.fedistream-tracklist__item[data-track-id]');
|
||||
const tracks = [];
|
||||
|
||||
items.forEach(item => {
|
||||
const trackId = item.dataset.trackId;
|
||||
const titleEl = item.querySelector('.fedistream-tracklist__title');
|
||||
const artistEl = item.querySelector('.fedistream-tracklist__artist');
|
||||
const artworkEl = item.querySelector('.fedistream-tracklist__artwork');
|
||||
|
||||
tracks.push({
|
||||
id: trackId,
|
||||
title: titleEl ? titleEl.textContent.trim() : 'Unknown Track',
|
||||
artist: artistEl ? artistEl.textContent.trim() : '',
|
||||
thumbnail: artworkEl ? artworkEl.src : ''
|
||||
});
|
||||
});
|
||||
|
||||
if (tracks.length === 0) return;
|
||||
|
||||
this.clearQueue();
|
||||
tracks.forEach(track => this.addToQueue(track));
|
||||
|
||||
if (shuffle) {
|
||||
this.shuffleQueue();
|
||||
}
|
||||
|
||||
this.playAtIndex(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add track to queue
|
||||
*/
|
||||
addToQueue(track) {
|
||||
this.queue.push(track);
|
||||
this.originalQueue.push(track);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear queue
|
||||
*/
|
||||
clearQueue() {
|
||||
this.queue = [];
|
||||
this.originalQueue = [];
|
||||
this.currentIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle queue
|
||||
*/
|
||||
shuffleQueue() {
|
||||
const currentTrack = this.currentIndex >= 0 ? this.queue[this.currentIndex] : null;
|
||||
|
||||
// Fisher-Yates shuffle
|
||||
for (let i = this.queue.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[this.queue[i], this.queue[j]] = [this.queue[j], this.queue[i]];
|
||||
}
|
||||
|
||||
// Keep current track at current position if playing
|
||||
if (currentTrack) {
|
||||
const newIndex = this.queue.indexOf(currentTrack);
|
||||
if (newIndex !== this.currentIndex) {
|
||||
[this.queue[this.currentIndex], this.queue[newIndex]] = [this.queue[newIndex], this.queue[this.currentIndex]];
|
||||
}
|
||||
}
|
||||
|
||||
this.isShuffle = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore original queue order
|
||||
*/
|
||||
restoreQueueOrder() {
|
||||
const currentTrack = this.currentIndex >= 0 ? this.queue[this.currentIndex] : null;
|
||||
this.queue = [...this.originalQueue];
|
||||
|
||||
if (currentTrack) {
|
||||
this.currentIndex = this.queue.findIndex(t => t.id === currentTrack.id);
|
||||
}
|
||||
|
||||
this.isShuffle = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play track at index
|
||||
*/
|
||||
async playAtIndex(index) {
|
||||
if (index < 0 || index >= this.queue.length) return;
|
||||
|
||||
const track = this.queue[index];
|
||||
this.currentIndex = index;
|
||||
|
||||
// Get audio URL if not present
|
||||
if (!track.audio_url) {
|
||||
const data = await this.fetchTrackData(track.id);
|
||||
if (data && data.audio_url) {
|
||||
track.audio_url = data.audio_url;
|
||||
} else {
|
||||
console.error('Could not load audio for track', track.id);
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.audio.src = track.audio_url;
|
||||
this.audio.load();
|
||||
this.audio.play().catch(e => {
|
||||
console.error('Playback failed', e);
|
||||
});
|
||||
|
||||
this.updateNowPlaying(track);
|
||||
this.updateTracklistHighlight();
|
||||
this.recordPlay(track.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle play/pause
|
||||
*/
|
||||
togglePlayPause() {
|
||||
if (this.isPlaying) {
|
||||
this.audio.pause();
|
||||
} else {
|
||||
if (this.audio.src) {
|
||||
this.audio.play().catch(e => console.error('Playback failed', e));
|
||||
} else if (this.queue.length > 0) {
|
||||
this.playAtIndex(this.currentIndex >= 0 ? this.currentIndex : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play next track
|
||||
*/
|
||||
next() {
|
||||
if (this.queue.length === 0) return;
|
||||
|
||||
let nextIndex = this.currentIndex + 1;
|
||||
|
||||
if (nextIndex >= this.queue.length) {
|
||||
if (this.repeatMode === 'all') {
|
||||
nextIndex = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.playAtIndex(nextIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play previous track
|
||||
*/
|
||||
previous() {
|
||||
if (this.queue.length === 0) return;
|
||||
|
||||
// If more than 3 seconds into track, restart it
|
||||
if (this.audio.currentTime > 3) {
|
||||
this.audio.currentTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let prevIndex = this.currentIndex - 1;
|
||||
|
||||
if (prevIndex < 0) {
|
||||
if (this.repeatMode === 'all') {
|
||||
prevIndex = this.queue.length - 1;
|
||||
} else {
|
||||
this.audio.currentTime = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.playAtIndex(prevIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle shuffle mode
|
||||
*/
|
||||
toggleShuffle() {
|
||||
if (this.isShuffle) {
|
||||
this.restoreQueueOrder();
|
||||
} else {
|
||||
this.shuffleQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle repeat mode
|
||||
*/
|
||||
toggleRepeat() {
|
||||
const modes = ['none', 'all', 'one'];
|
||||
const currentModeIndex = modes.indexOf(this.repeatMode);
|
||||
this.repeatMode = modes[(currentModeIndex + 1) % modes.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set volume
|
||||
*/
|
||||
setVolume(value) {
|
||||
this.volume = Math.max(0, Math.min(1, value));
|
||||
this.audio.volume = this.volume;
|
||||
this.audio.muted = false;
|
||||
this.saveVolumeToStorage();
|
||||
this.updateVolumeUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle mute
|
||||
*/
|
||||
toggleMute() {
|
||||
this.audio.muted = !this.audio.muted;
|
||||
this.updateVolumeUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save volume to localStorage
|
||||
*/
|
||||
saveVolumeToStorage() {
|
||||
try {
|
||||
localStorage.setItem('fedistream_volume', this.volume.toString());
|
||||
} catch (e) {
|
||||
// Storage not available
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load volume from localStorage
|
||||
*/
|
||||
loadVolumeFromStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem('fedistream_volume');
|
||||
if (stored !== null) {
|
||||
this.volume = parseFloat(stored);
|
||||
this.audio.volume = this.volume;
|
||||
this.updateVolumeUI();
|
||||
}
|
||||
} catch (e) {
|
||||
// Storage not available
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update volume UI
|
||||
*/
|
||||
updateVolumeUI() {
|
||||
const sliders = document.querySelectorAll('.fedistream-player__volume-slider');
|
||||
sliders.forEach(slider => {
|
||||
slider.value = this.audio.muted ? 0 : this.volume * 100;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time in mm:ss
|
||||
*/
|
||||
formatTime(seconds) {
|
||||
if (isNaN(seconds) || !isFinite(seconds)) return '0:00';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update now playing info in all widgets
|
||||
*/
|
||||
updateNowPlaying(track) {
|
||||
// Update all now playing widgets
|
||||
this.nowPlayingWidgets.forEach(widget => {
|
||||
const idle = widget.querySelector('.fedistream-now-playing__idle');
|
||||
const active = widget.querySelector('.fedistream-now-playing__active');
|
||||
|
||||
if (idle) idle.style.display = 'none';
|
||||
if (active) active.style.display = 'block';
|
||||
|
||||
const artwork = widget.querySelector('.fedistream-now-playing__artwork');
|
||||
const title = widget.querySelector('.fedistream-now-playing__title');
|
||||
const artist = widget.querySelector('.fedistream-now-playing__artist');
|
||||
|
||||
if (artwork && track.thumbnail) artwork.src = track.thumbnail;
|
||||
if (title) title.textContent = track.title;
|
||||
if (artist) artist.textContent = track.artist;
|
||||
});
|
||||
|
||||
// Update multi-track player current info
|
||||
const multiPlayers = document.querySelectorAll('.fedistream-player--multi');
|
||||
multiPlayers.forEach(player => {
|
||||
const artwork = player.querySelector('.fedistream-player__artwork--current');
|
||||
const title = player.querySelector('.fedistream-player__title--current');
|
||||
const artist = player.querySelector('.fedistream-player__artist--current');
|
||||
|
||||
if (artwork && track.thumbnail) artwork.src = track.thumbnail;
|
||||
if (title) title.textContent = track.title;
|
||||
if (artist) artist.textContent = track.artist;
|
||||
});
|
||||
|
||||
// Update document title
|
||||
if (track.title) {
|
||||
document.title = `${track.title}${track.artist ? ' - ' + track.artist : ''} | ${document.title.split('|').pop().trim()}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update tracklist item highlighting
|
||||
*/
|
||||
updateTracklistHighlight() {
|
||||
// Remove all highlights
|
||||
document.querySelectorAll('.fedistream-tracklist__item.is-playing').forEach(el => {
|
||||
el.classList.remove('is-playing');
|
||||
});
|
||||
|
||||
// Add highlight to current track
|
||||
if (this.currentIndex >= 0 && this.queue[this.currentIndex]) {
|
||||
const trackId = this.queue[this.currentIndex].id;
|
||||
document.querySelectorAll(`.fedistream-tracklist__item[data-track-id="${trackId}"]`).forEach(el => {
|
||||
el.classList.add('is-playing');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record track play via AJAX
|
||||
*/
|
||||
recordPlay(trackId) {
|
||||
if (!window.wpFediStream || !window.wpFediStream.ajaxUrl) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('action', 'fedistream_record_play');
|
||||
formData.append('track_id', trackId);
|
||||
formData.append('nonce', window.wpFediStream.nonce);
|
||||
|
||||
fetch(window.wpFediStream.ajaxUrl, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
}).catch(e => console.error('Failed to record play', e));
|
||||
}
|
||||
|
||||
// Audio event handlers
|
||||
|
||||
onTimeUpdate() {
|
||||
const current = this.audio.currentTime;
|
||||
const duration = this.audio.duration || 0;
|
||||
const percent = duration ? (current / duration) * 100 : 0;
|
||||
|
||||
// Update progress bars
|
||||
document.querySelectorAll('.fedistream-player__bar-progress').forEach(el => {
|
||||
el.style.width = `${percent}%`;
|
||||
});
|
||||
|
||||
document.querySelectorAll('.fedistream-player__seek').forEach(el => {
|
||||
el.value = percent;
|
||||
});
|
||||
|
||||
// Update time displays
|
||||
document.querySelectorAll('.fedistream-player__time--current').forEach(el => {
|
||||
el.textContent = this.formatTime(current);
|
||||
});
|
||||
|
||||
// Update now playing widgets progress
|
||||
this.nowPlayingWidgets.forEach(widget => {
|
||||
const progress = widget.querySelector('.fedistream-now-playing__bar-progress');
|
||||
const currentTime = widget.querySelector('.fedistream-now-playing__time--current');
|
||||
const totalTime = widget.querySelector('.fedistream-now-playing__time--total');
|
||||
|
||||
if (progress) progress.style.width = `${percent}%`;
|
||||
if (currentTime) currentTime.textContent = this.formatTime(current);
|
||||
if (totalTime) totalTime.textContent = this.formatTime(duration);
|
||||
});
|
||||
}
|
||||
|
||||
onLoadedMetadata() {
|
||||
const duration = this.audio.duration || 0;
|
||||
|
||||
document.querySelectorAll('.fedistream-player__time--total').forEach(el => {
|
||||
el.textContent = this.formatTime(duration);
|
||||
});
|
||||
}
|
||||
|
||||
onEnded() {
|
||||
if (this.repeatMode === 'one') {
|
||||
this.audio.currentTime = 0;
|
||||
this.audio.play().catch(e => console.error('Playback failed', e));
|
||||
} else {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
onPlay() {
|
||||
this.isPlaying = true;
|
||||
|
||||
// Update all player UIs
|
||||
document.querySelectorAll('.fedistream-player').forEach(el => {
|
||||
el.classList.add('is-playing');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.fedistream-now-playing').forEach(el => {
|
||||
el.classList.add('is-playing');
|
||||
});
|
||||
}
|
||||
|
||||
onPause() {
|
||||
this.isPlaying = false;
|
||||
|
||||
document.querySelectorAll('.fedistream-player').forEach(el => {
|
||||
el.classList.remove('is-playing');
|
||||
});
|
||||
|
||||
document.querySelectorAll('.fedistream-now-playing').forEach(el => {
|
||||
el.classList.remove('is-playing');
|
||||
});
|
||||
}
|
||||
|
||||
onError(e) {
|
||||
console.error('Audio error', e);
|
||||
// Try next track on error
|
||||
if (this.queue.length > 1) {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
onWaiting() {
|
||||
document.querySelectorAll('.fedistream-player').forEach(el => {
|
||||
el.classList.add('is-loading');
|
||||
});
|
||||
}
|
||||
|
||||
onCanPlay() {
|
||||
document.querySelectorAll('.fedistream-player').forEach(el => {
|
||||
el.classList.remove('is-loading');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize when DOM is ready
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize the global player
|
||||
window.fediStreamPlayer = new FediStreamPlayer();
|
||||
|
||||
// Initialize any inline players
|
||||
window.fediStreamPlayer.initInlinePlayers();
|
||||
window.fediStreamPlayer.initMultiTrackPlayers();
|
||||
});
|
||||
|
||||
// Media Session API for system controls
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.setActionHandler('play', () => {
|
||||
if (window.fediStreamPlayer) {
|
||||
window.fediStreamPlayer.togglePlayPause();
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('pause', () => {
|
||||
if (window.fediStreamPlayer) {
|
||||
window.fediStreamPlayer.togglePlayPause();
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||
if (window.fediStreamPlayer) {
|
||||
window.fediStreamPlayer.previous();
|
||||
}
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||
if (window.fediStreamPlayer) {
|
||||
window.fediStreamPlayer.next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
1
assets/js/index.php
Normal file
1
assets/js/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
554
assets/js/library.js
Normal file
554
assets/js/library.js
Normal file
@@ -0,0 +1,554 @@
|
||||
/**
|
||||
* FediStream Library JavaScript
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
( function( $ ) {
|
||||
'use strict';
|
||||
|
||||
const Library = {
|
||||
currentTab: 'favorites',
|
||||
currentPage: { favorites: 1, artists: 1, history: 1 },
|
||||
currentFilter: 'all',
|
||||
isLoading: false,
|
||||
|
||||
init: function() {
|
||||
this.bindEvents();
|
||||
this.loadInitialTab();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
const self = this;
|
||||
|
||||
// Tab navigation.
|
||||
$( '.fedistream-library-nav .tab-btn' ).on( 'click', function() {
|
||||
const tab = $( this ).data( 'tab' );
|
||||
self.switchTab( tab );
|
||||
} );
|
||||
|
||||
// Filter change.
|
||||
$( '.fedistream-library-filters .filter-type' ).on( 'change', function() {
|
||||
self.currentFilter = $( this ).val();
|
||||
self.currentPage.favorites = 1;
|
||||
self.loadFavorites();
|
||||
} );
|
||||
|
||||
// Clear history.
|
||||
$( '.btn-clear-history' ).on( 'click', function() {
|
||||
self.clearHistory();
|
||||
} );
|
||||
|
||||
// Pagination clicks.
|
||||
$( document ).on( 'click', '.library-pagination .page-btn', function() {
|
||||
const page = $( this ).data( 'page' );
|
||||
const tab = $( this ).closest( '.library-pagination' ).data( 'tab' );
|
||||
self.goToPage( tab, page );
|
||||
} );
|
||||
|
||||
// Unfavorite button.
|
||||
$( document ).on( 'click', '.unfavorite-btn', function() {
|
||||
const btn = $( this );
|
||||
const contentType = btn.data( 'content-type' );
|
||||
const contentId = btn.data( 'content-id' );
|
||||
self.toggleFavorite( contentType, contentId, btn.closest( '.library-item' ) );
|
||||
} );
|
||||
|
||||
// Unfollow button.
|
||||
$( document ).on( 'click', '.unfollow-btn', function() {
|
||||
const btn = $( this );
|
||||
const artistId = btn.data( 'artist-id' );
|
||||
self.toggleFollow( artistId, btn.closest( '.library-item' ) );
|
||||
} );
|
||||
|
||||
// Play button.
|
||||
$( document ).on( 'click', '.play-btn', function( e ) {
|
||||
e.preventDefault();
|
||||
const trackId = $( this ).data( 'track-id' );
|
||||
self.playTrack( trackId );
|
||||
} );
|
||||
},
|
||||
|
||||
loadInitialTab: function() {
|
||||
const initialTab = $( '.fedistream-library' ).data( 'initial-tab' ) || 'favorites';
|
||||
this.switchTab( initialTab );
|
||||
},
|
||||
|
||||
switchTab: function( tab ) {
|
||||
this.currentTab = tab;
|
||||
|
||||
// Update nav.
|
||||
$( '.fedistream-library-nav .tab-btn' ).removeClass( 'active' );
|
||||
$( '.fedistream-library-nav .tab-btn[data-tab="' + tab + '"]' ).addClass( 'active' );
|
||||
|
||||
// Update content.
|
||||
$( '.tab-content' ).removeClass( 'active' );
|
||||
$( '#tab-' + tab ).addClass( 'active' );
|
||||
|
||||
// Show/hide filters.
|
||||
if ( tab === 'favorites' ) {
|
||||
$( '.fedistream-library-filters' ).show();
|
||||
} else {
|
||||
$( '.fedistream-library-filters' ).hide();
|
||||
}
|
||||
|
||||
// Load content.
|
||||
this.loadTabContent( tab );
|
||||
},
|
||||
|
||||
loadTabContent: function( tab ) {
|
||||
switch ( tab ) {
|
||||
case 'favorites':
|
||||
this.loadFavorites();
|
||||
break;
|
||||
case 'artists':
|
||||
this.loadArtists();
|
||||
break;
|
||||
case 'history':
|
||||
this.loadHistory();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
loadFavorites: function() {
|
||||
const self = this;
|
||||
|
||||
if ( this.isLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_library',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
type: this.currentFilter,
|
||||
page: this.currentPage.favorites
|
||||
},
|
||||
success: function( response ) {
|
||||
self.hideLoading();
|
||||
|
||||
if ( response.success ) {
|
||||
self.renderFavorites( response.data );
|
||||
} else {
|
||||
self.showError( response.data.message );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.hideLoading();
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
loadArtists: function() {
|
||||
const self = this;
|
||||
|
||||
if ( this.isLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_followed_artists',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
page: this.currentPage.artists
|
||||
},
|
||||
success: function( response ) {
|
||||
self.hideLoading();
|
||||
|
||||
if ( response.success ) {
|
||||
self.renderArtists( response.data );
|
||||
} else {
|
||||
self.showError( response.data.message );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.hideLoading();
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
loadHistory: function() {
|
||||
const self = this;
|
||||
|
||||
if ( this.isLoading ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showLoading();
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_history',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
page: this.currentPage.history
|
||||
},
|
||||
success: function( response ) {
|
||||
self.hideLoading();
|
||||
|
||||
if ( response.success ) {
|
||||
self.renderHistory( response.data );
|
||||
} else {
|
||||
self.showError( response.data.message );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.hideLoading();
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
renderFavorites: function( data ) {
|
||||
const container = $( '.favorites-grid' );
|
||||
container.empty();
|
||||
|
||||
if ( ! data.items || data.items.length === 0 ) {
|
||||
container.html( '<p class="empty-message">' + fedistreamLibrary.i18n.noFavorites + '</p>' );
|
||||
$( '.library-pagination[data-tab="favorites"]' ).empty();
|
||||
return;
|
||||
}
|
||||
|
||||
data.items.forEach( function( item ) {
|
||||
container.append( this.createFavoriteItem( item ) );
|
||||
}, this );
|
||||
|
||||
this.renderPagination( 'favorites', data );
|
||||
},
|
||||
|
||||
renderArtists: function( data ) {
|
||||
const container = $( '.artists-grid' );
|
||||
container.empty();
|
||||
|
||||
if ( ! data.artists || data.artists.length === 0 ) {
|
||||
container.html( '<p class="empty-message">' + fedistreamLibrary.i18n.noArtists + '</p>' );
|
||||
$( '.library-pagination[data-tab="artists"]' ).empty();
|
||||
return;
|
||||
}
|
||||
|
||||
data.artists.forEach( function( artist ) {
|
||||
container.append( this.createArtistItem( artist ) );
|
||||
}, this );
|
||||
|
||||
this.renderPagination( 'artists', data );
|
||||
},
|
||||
|
||||
renderHistory: function( data ) {
|
||||
const container = $( '.history-list' );
|
||||
container.empty();
|
||||
|
||||
if ( ! data.tracks || data.tracks.length === 0 ) {
|
||||
container.html( '<p class="empty-message">' + fedistreamLibrary.i18n.noHistory + '</p>' );
|
||||
$( '.library-pagination[data-tab="history"]' ).empty();
|
||||
return;
|
||||
}
|
||||
|
||||
data.tracks.forEach( function( track ) {
|
||||
container.append( this.createHistoryItem( track ) );
|
||||
}, this );
|
||||
|
||||
this.renderPagination( 'history', data );
|
||||
},
|
||||
|
||||
createFavoriteItem: function( item ) {
|
||||
const thumbnail = item.thumbnail
|
||||
? '<img src="' + item.thumbnail + '" alt="' + this.escapeHtml( item.title ) + '">'
|
||||
: '<div class="placeholder-thumbnail"><span class="dashicons dashicons-format-audio"></span></div>';
|
||||
|
||||
const playBtn = item.type === 'track'
|
||||
? '<button class="play-btn" data-track-id="' + item.id + '"><span class="dashicons dashicons-controls-play"></span></button>'
|
||||
: '';
|
||||
|
||||
const artistInfo = item.artist
|
||||
? '<p class="item-artist">' + this.escapeHtml( item.artist ) + '</p>'
|
||||
: '';
|
||||
|
||||
let metaInfo = '';
|
||||
if ( item.type === 'track' && item.duration ) {
|
||||
metaInfo = '<p class="item-duration">' + this.formatDuration( item.duration ) + '</p>';
|
||||
} else if ( item.type === 'album' && item.track_count ) {
|
||||
metaInfo = '<p class="item-tracks">' + item.track_count + ' tracks</p>';
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="library-item favorite-item" data-type="${item.type}" data-id="${item.id}">
|
||||
<div class="item-thumbnail">
|
||||
${thumbnail}
|
||||
${playBtn}
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="${item.permalink}">${this.escapeHtml( item.title )}</a>
|
||||
</h4>
|
||||
${artistInfo}
|
||||
${metaInfo}
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="unfavorite-btn" data-content-type="${item.type}" data-content-id="${item.id}" title="Remove from library">
|
||||
<span class="dashicons dashicons-heart"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
createArtistItem: function( artist ) {
|
||||
const thumbnail = artist.thumbnail
|
||||
? '<img src="' + artist.thumbnail + '" alt="' + this.escapeHtml( artist.name ) + '">'
|
||||
: '<div class="placeholder-thumbnail"><span class="dashicons dashicons-admin-users"></span></div>';
|
||||
|
||||
const typeLabel = artist.type === 'band' ? 'Band' : 'Artist';
|
||||
|
||||
return `
|
||||
<div class="library-item artist-item" data-id="${artist.id}">
|
||||
<div class="item-thumbnail artist-avatar">
|
||||
${thumbnail}
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="${artist.permalink}">${this.escapeHtml( artist.name )}</a>
|
||||
</h4>
|
||||
<p class="item-type">${typeLabel}</p>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="unfollow-btn" data-artist-id="${artist.id}" title="Unfollow">
|
||||
<span class="dashicons dashicons-minus"></span>
|
||||
<span class="button-text">Unfollow</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
createHistoryItem: function( track ) {
|
||||
const thumbnail = track.thumbnail
|
||||
? '<img src="' + track.thumbnail + '" alt="' + this.escapeHtml( track.title ) + '">'
|
||||
: '<div class="placeholder-thumbnail"><span class="dashicons dashicons-format-audio"></span></div>';
|
||||
|
||||
const artistInfo = track.artist
|
||||
? '<p class="item-artist">' + this.escapeHtml( track.artist ) + '</p>'
|
||||
: '';
|
||||
|
||||
const duration = track.duration
|
||||
? '<span class="item-duration">' + this.formatDuration( track.duration ) + '</span>'
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="library-item history-item" data-id="${track.id}">
|
||||
<div class="item-thumbnail">
|
||||
${thumbnail}
|
||||
<button class="play-btn" data-track-id="${track.id}">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="${track.permalink}">${this.escapeHtml( track.title )}</a>
|
||||
</h4>
|
||||
${artistInfo}
|
||||
<p class="item-played">${this.formatPlayedTime( track.played_at )}</p>
|
||||
</div>
|
||||
<div class="item-meta">
|
||||
${duration}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
renderPagination: function( tab, data ) {
|
||||
const container = $( '.library-pagination[data-tab="' + tab + '"]' );
|
||||
container.empty();
|
||||
|
||||
if ( data.total_pages <= 1 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="pagination-controls">';
|
||||
|
||||
// Previous button.
|
||||
if ( data.page > 1 ) {
|
||||
html += '<button class="page-btn prev" data-page="' + ( data.page - 1 ) + '">« Previous</button>';
|
||||
}
|
||||
|
||||
// Page numbers.
|
||||
html += '<span class="page-info">Page ' + data.page + ' of ' + data.total_pages + '</span>';
|
||||
|
||||
// Next button.
|
||||
if ( data.page < data.total_pages ) {
|
||||
html += '<button class="page-btn next" data-page="' + ( data.page + 1 ) + '">Next »</button>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
container.html( html );
|
||||
},
|
||||
|
||||
goToPage: function( tab, page ) {
|
||||
this.currentPage[ tab ] = page;
|
||||
this.loadTabContent( tab );
|
||||
},
|
||||
|
||||
toggleFavorite: function( contentType, contentId, element ) {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_toggle_favorite',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
content_type: contentType,
|
||||
content_id: contentId
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success && response.data.action === 'removed' ) {
|
||||
element.fadeOut( 300, function() {
|
||||
$( this ).remove();
|
||||
self.updateFavoriteCount();
|
||||
} );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
toggleFollow: function( artistId, element ) {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_toggle_follow',
|
||||
nonce: fedistreamLibrary.nonce,
|
||||
artist_id: artistId
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success && response.data.action === 'unfollowed' ) {
|
||||
element.fadeOut( 300, function() {
|
||||
$( this ).remove();
|
||||
self.updateFollowingCount();
|
||||
} );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
clearHistory: function() {
|
||||
const self = this;
|
||||
|
||||
if ( ! confirm( fedistreamLibrary.i18n.confirmClear ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamLibrary.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_clear_history',
|
||||
nonce: fedistreamLibrary.nonce
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
$( '.history-list' ).html( '<p class="empty-message">' + fedistreamLibrary.i18n.noHistory + '</p>' );
|
||||
$( '.library-pagination[data-tab="history"]' ).empty();
|
||||
} else {
|
||||
self.showError( response.data.message );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
self.showError( fedistreamLibrary.i18n.error );
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
playTrack: function( trackId ) {
|
||||
// Trigger global player if available.
|
||||
if ( window.FediStreamPlayer && typeof window.FediStreamPlayer.playTrack === 'function' ) {
|
||||
window.FediStreamPlayer.playTrack( trackId );
|
||||
} else {
|
||||
// Fallback: redirect to track page.
|
||||
window.location.href = '?p=' + trackId;
|
||||
}
|
||||
},
|
||||
|
||||
updateFavoriteCount: function() {
|
||||
const count = $( '.favorites-grid .library-item' ).length;
|
||||
$( '.tab-btn[data-tab="favorites"] .count' ).text( count );
|
||||
},
|
||||
|
||||
updateFollowingCount: function() {
|
||||
const count = $( '.artists-grid .library-item' ).length;
|
||||
$( '.tab-btn[data-tab="artists"] .count' ).text( count );
|
||||
},
|
||||
|
||||
showLoading: function() {
|
||||
this.isLoading = true;
|
||||
$( '.fedistream-library-loading' ).show();
|
||||
},
|
||||
|
||||
hideLoading: function() {
|
||||
this.isLoading = false;
|
||||
$( '.fedistream-library-loading' ).hide();
|
||||
},
|
||||
|
||||
showError: function( message ) {
|
||||
alert( message );
|
||||
},
|
||||
|
||||
formatDuration: function( seconds ) {
|
||||
const mins = Math.floor( seconds / 60 );
|
||||
const secs = seconds % 60;
|
||||
return mins + ':' + ( secs < 10 ? '0' : '' ) + secs;
|
||||
},
|
||||
|
||||
formatPlayedTime: function( dateString ) {
|
||||
const date = new Date( dateString );
|
||||
const now = new Date();
|
||||
const diff = Math.floor( ( now - date ) / 1000 );
|
||||
|
||||
if ( diff < 60 ) {
|
||||
return 'Just now';
|
||||
} else if ( diff < 3600 ) {
|
||||
const mins = Math.floor( diff / 60 );
|
||||
return mins + ' minute' + ( mins !== 1 ? 's' : '' ) + ' ago';
|
||||
} else if ( diff < 86400 ) {
|
||||
const hours = Math.floor( diff / 3600 );
|
||||
return hours + ' hour' + ( hours !== 1 ? 's' : '' ) + ' ago';
|
||||
} else {
|
||||
const days = Math.floor( diff / 86400 );
|
||||
return days + ' day' + ( days !== 1 ? 's' : '' ) + ' ago';
|
||||
}
|
||||
},
|
||||
|
||||
escapeHtml: function( text ) {
|
||||
const div = document.createElement( 'div' );
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready.
|
||||
$( document ).ready( function() {
|
||||
if ( $( '.fedistream-library' ).length ) {
|
||||
Library.init();
|
||||
}
|
||||
} );
|
||||
|
||||
} )( jQuery );
|
||||
353
assets/js/notifications.js
Normal file
353
assets/js/notifications.js
Normal file
@@ -0,0 +1,353 @@
|
||||
/**
|
||||
* FediStream Notifications JavaScript
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
( function( $ ) {
|
||||
'use strict';
|
||||
|
||||
const Notifications = {
|
||||
unreadCount: 0,
|
||||
isOpen: false,
|
||||
pollTimer: null,
|
||||
|
||||
init: function() {
|
||||
this.createDropdown();
|
||||
this.bindEvents();
|
||||
this.loadNotifications();
|
||||
this.startPolling();
|
||||
},
|
||||
|
||||
createDropdown: function() {
|
||||
const dropdown = `
|
||||
<div class="fedistream-notifications-dropdown" style="display: none;">
|
||||
<div class="notifications-header">
|
||||
<h4>${fedistreamNotifications.i18n.viewAll || 'Notifications'}</h4>
|
||||
<button class="mark-all-read" title="${fedistreamNotifications.i18n.markAllRead}">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="notifications-list">
|
||||
<div class="loading">${fedistreamNotifications.i18n.loading || 'Loading...'}</div>
|
||||
</div>
|
||||
<div class="notifications-footer">
|
||||
<a href="#" class="view-all">${fedistreamNotifications.i18n.viewAll}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$( '.fedistream-notifications-menu' ).append( dropdown );
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
const self = this;
|
||||
|
||||
// Toggle dropdown.
|
||||
$( document ).on( 'click', '#wp-admin-bar-fedistream-notifications > a', function( e ) {
|
||||
e.preventDefault();
|
||||
self.toggleDropdown();
|
||||
} );
|
||||
|
||||
// Close dropdown when clicking outside.
|
||||
$( document ).on( 'click', function( e ) {
|
||||
if ( ! $( e.target ).closest( '.fedistream-notifications-menu' ).length ) {
|
||||
self.closeDropdown();
|
||||
}
|
||||
} );
|
||||
|
||||
// Mark all as read.
|
||||
$( document ).on( 'click', '.fedistream-notifications-dropdown .mark-all-read', function( e ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
self.markAllRead();
|
||||
} );
|
||||
|
||||
// Mark single as read.
|
||||
$( document ).on( 'click', '.notification-item', function() {
|
||||
const id = $( this ).data( 'id' );
|
||||
const isRead = $( this ).hasClass( 'is-read' );
|
||||
|
||||
if ( ! isRead ) {
|
||||
self.markRead( id, $( this ) );
|
||||
}
|
||||
} );
|
||||
|
||||
// Delete notification.
|
||||
$( document ).on( 'click', '.notification-item .delete-btn', function( e ) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const item = $( this ).closest( '.notification-item' );
|
||||
const id = item.data( 'id' );
|
||||
self.deleteNotification( id, item );
|
||||
} );
|
||||
},
|
||||
|
||||
toggleDropdown: function() {
|
||||
if ( this.isOpen ) {
|
||||
this.closeDropdown();
|
||||
} else {
|
||||
this.openDropdown();
|
||||
}
|
||||
},
|
||||
|
||||
openDropdown: function() {
|
||||
$( '.fedistream-notifications-dropdown' ).show();
|
||||
this.isOpen = true;
|
||||
this.loadNotifications();
|
||||
},
|
||||
|
||||
closeDropdown: function() {
|
||||
$( '.fedistream-notifications-dropdown' ).hide();
|
||||
this.isOpen = false;
|
||||
},
|
||||
|
||||
loadNotifications: function() {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_notifications',
|
||||
nonce: fedistreamNotifications.nonce,
|
||||
limit: 10
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
self.renderNotifications( response.data.notifications );
|
||||
self.updateUnreadCount( response.data.unread_count );
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
$( '.notifications-list' ).html(
|
||||
'<p class="error">' + fedistreamNotifications.i18n.error + '</p>'
|
||||
);
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
renderNotifications: function( notifications ) {
|
||||
const container = $( '.notifications-list' );
|
||||
container.empty();
|
||||
|
||||
if ( ! notifications || notifications.length === 0 ) {
|
||||
container.html(
|
||||
'<p class="empty">' + fedistreamNotifications.i18n.noNotifications + '</p>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.forEach( function( notification ) {
|
||||
container.append( this.createNotificationItem( notification ) );
|
||||
}, this );
|
||||
},
|
||||
|
||||
createNotificationItem: function( notification ) {
|
||||
const isRead = notification.is_read ? 'is-read' : '';
|
||||
const icon = this.getNotificationIcon( notification.type );
|
||||
const time = this.formatTime( notification.created_at );
|
||||
const link = this.getNotificationLink( notification );
|
||||
|
||||
return `
|
||||
<div class="notification-item ${isRead}" data-id="${notification.id}">
|
||||
<div class="notification-icon">
|
||||
<span class="dashicons dashicons-${icon}"></span>
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
<div class="notification-title">${this.escapeHtml( notification.title )}</div>
|
||||
<div class="notification-message">${this.escapeHtml( notification.message )}</div>
|
||||
<div class="notification-time">${time}</div>
|
||||
</div>
|
||||
<div class="notification-actions">
|
||||
<button class="delete-btn" title="Delete">
|
||||
<span class="dashicons dashicons-no-alt"></span>
|
||||
</button>
|
||||
</div>
|
||||
${link ? `<a href="${link}" class="notification-link"></a>` : ''}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
getNotificationIcon: function( type ) {
|
||||
const icons = {
|
||||
'new_release': 'album',
|
||||
'new_follower': 'admin-users',
|
||||
'fediverse_like': 'heart',
|
||||
'fediverse_boost': 'megaphone',
|
||||
'playlist_added': 'playlist-audio',
|
||||
'purchase': 'cart',
|
||||
'system': 'info'
|
||||
};
|
||||
return icons[ type ] || 'bell';
|
||||
},
|
||||
|
||||
getNotificationLink: function( notification ) {
|
||||
const data = notification.data || {};
|
||||
|
||||
if ( data.album_url ) {
|
||||
return data.album_url;
|
||||
}
|
||||
if ( data.track_url ) {
|
||||
return data.track_url;
|
||||
}
|
||||
if ( data.artist_url ) {
|
||||
return data.artist_url;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
markRead: function( notificationId, element ) {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_mark_notification_read',
|
||||
nonce: fedistreamNotifications.nonce,
|
||||
notification_id: notificationId
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
element.addClass( 'is-read' );
|
||||
self.updateUnreadCount( response.data.unread_count );
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
markAllRead: function() {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_mark_all_notifications_read',
|
||||
nonce: fedistreamNotifications.nonce
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
$( '.notification-item' ).addClass( 'is-read' );
|
||||
self.updateUnreadCount( 0 );
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
deleteNotification: function( notificationId, element ) {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_delete_notification',
|
||||
nonce: fedistreamNotifications.nonce,
|
||||
notification_id: notificationId
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
element.fadeOut( 200, function() {
|
||||
$( this ).remove();
|
||||
|
||||
// Check if list is empty.
|
||||
if ( $( '.notification-item' ).length === 0 ) {
|
||||
$( '.notifications-list' ).html(
|
||||
'<p class="empty">' + fedistreamNotifications.i18n.noNotifications + '</p>'
|
||||
);
|
||||
}
|
||||
} );
|
||||
self.updateUnreadCount( response.data.unread_count );
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
updateUnreadCount: function( count ) {
|
||||
this.unreadCount = count;
|
||||
|
||||
const badge = $( '.fedistream-notification-count' );
|
||||
|
||||
if ( count > 0 ) {
|
||||
if ( badge.length ) {
|
||||
badge.text( count );
|
||||
} else {
|
||||
$( '#wp-admin-bar-fedistream-notifications .ab-icon' ).after(
|
||||
'<span class="fedistream-notification-count">' + count + '</span>'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
badge.remove();
|
||||
}
|
||||
},
|
||||
|
||||
startPolling: function() {
|
||||
const self = this;
|
||||
const interval = fedistreamNotifications.pollInterval || 60000;
|
||||
|
||||
this.pollTimer = setInterval( function() {
|
||||
if ( ! self.isOpen ) {
|
||||
self.checkForNewNotifications();
|
||||
}
|
||||
}, interval );
|
||||
},
|
||||
|
||||
checkForNewNotifications: function() {
|
||||
const self = this;
|
||||
|
||||
$.ajax( {
|
||||
url: fedistreamNotifications.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'fedistream_get_notifications',
|
||||
nonce: fedistreamNotifications.nonce,
|
||||
unread_only: true,
|
||||
limit: 1
|
||||
},
|
||||
success: function( response ) {
|
||||
if ( response.success ) {
|
||||
self.updateUnreadCount( response.data.unread_count );
|
||||
}
|
||||
}
|
||||
} );
|
||||
},
|
||||
|
||||
formatTime: function( dateString ) {
|
||||
const date = new Date( dateString );
|
||||
const now = new Date();
|
||||
const diff = Math.floor( ( now - date ) / 1000 );
|
||||
|
||||
if ( diff < 60 ) {
|
||||
return fedistreamNotifications.i18n.justNow || 'Just now';
|
||||
} else if ( diff < 3600 ) {
|
||||
const mins = Math.floor( diff / 60 );
|
||||
return mins + 'm ago';
|
||||
} else if ( diff < 86400 ) {
|
||||
const hours = Math.floor( diff / 3600 );
|
||||
return hours + 'h ago';
|
||||
} else if ( diff < 604800 ) {
|
||||
const days = Math.floor( diff / 86400 );
|
||||
return days + 'd ago';
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
},
|
||||
|
||||
escapeHtml: function( text ) {
|
||||
const div = document.createElement( 'div' );
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready.
|
||||
$( document ).ready( function() {
|
||||
if ( $( '.fedistream-notifications-menu' ).length ) {
|
||||
Notifications.init();
|
||||
}
|
||||
} );
|
||||
|
||||
} )( jQuery );
|
||||
54
composer.json
Normal file
54
composer.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "magdev/wp-fedistream",
|
||||
"description": "Stream music over ActivityPub - Build your own music streaming platform for Musicians and Labels",
|
||||
"type": "wordpress-plugin",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Marco Graetsch",
|
||||
"email": "magdev3.0@gmail.com",
|
||||
"homepage": "https://src.bundespruefstelle.ch/magdev"
|
||||
}
|
||||
],
|
||||
"homepage": "https://src.bundespruefstelle.ch/magdev/wp-fedistream",
|
||||
"support": {
|
||||
"issues": "https://src.bundespruefstelle.ch/magdev/wp-fedistream/issues"
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.0",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"wp-coding-standards/wpcs": "^3.0",
|
||||
"phpcompatibility/phpcompatibility-wp": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"WP_FediStream\\": "includes/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"WP_FediStream\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
},
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "8.3.0"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"phpcs": "phpcs",
|
||||
"phpcbf": "phpcbf",
|
||||
"test": "phpunit"
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
2637
composer.lock
generated
Normal file
2637
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
433
includes/ActivityPub/AlbumTransformer.php
Normal file
433
includes/ActivityPub/AlbumTransformer.php
Normal file
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
/**
|
||||
* Album Transformer for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Album posts to ActivityPub Collection objects.
|
||||
*/
|
||||
class AlbumTransformer {
|
||||
|
||||
/**
|
||||
* The album post.
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
protected \WP_Post $post;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WP_Post $post The album post.
|
||||
*/
|
||||
public function __construct( \WP_Post $post ) {
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'Collection';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object ID (URI).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object name (title).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
$album_type = get_post_meta( $this->post->ID, '_fedistream_album_type', true );
|
||||
$type_label = '';
|
||||
|
||||
switch ( $album_type ) {
|
||||
case 'ep':
|
||||
$type_label = ' (EP)';
|
||||
break;
|
||||
case 'single':
|
||||
$type_label = ' (Single)';
|
||||
break;
|
||||
case 'compilation':
|
||||
$type_label = ' (Compilation)';
|
||||
break;
|
||||
}
|
||||
|
||||
return $this->post->post_title . $type_label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content (liner notes/description).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
$content = $this->post->post_content;
|
||||
|
||||
// Apply content filters for proper formatting.
|
||||
$content = apply_filters( 'the_content', $content );
|
||||
|
||||
return wp_kses_post( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summary (excerpt).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_summary(): string {
|
||||
if ( ! empty( $this->post->post_excerpt ) ) {
|
||||
return wp_strip_all_tags( $this->post->post_excerpt );
|
||||
}
|
||||
|
||||
// Build summary from album info.
|
||||
$artist_id = get_post_meta( $this->post->ID, '_fedistream_album_artist', true );
|
||||
$artist = $artist_id ? get_post( $artist_id ) : null;
|
||||
|
||||
$release_date = get_post_meta( $this->post->ID, '_fedistream_release_date', true );
|
||||
$track_count = (int) get_post_meta( $this->post->ID, '_fedistream_total_tracks', true );
|
||||
|
||||
$summary = '';
|
||||
if ( $artist ) {
|
||||
$summary .= sprintf( __( 'By %s', 'wp-fedistream' ), $artist->post_title );
|
||||
}
|
||||
if ( $release_date ) {
|
||||
/* translators: %s: release date */
|
||||
$summary .= ' ' . sprintf( __( '- Released %s', 'wp-fedistream' ), $release_date );
|
||||
}
|
||||
if ( $track_count ) {
|
||||
/* translators: %d: number of tracks */
|
||||
$summary .= ' ' . sprintf( _n( '- %d track', '- %d tracks', $track_count, 'wp-fedistream' ), $track_count );
|
||||
}
|
||||
|
||||
return trim( $summary );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL (permalink).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributed actor.
|
||||
*
|
||||
* @return string Artist URI.
|
||||
*/
|
||||
public function get_attributed_to(): string {
|
||||
$artist_id = get_post_meta( $this->post->ID, '_fedistream_album_artist', true );
|
||||
|
||||
if ( ! $artist_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_permalink( $artist_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the published date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_published(): string {
|
||||
// Use release date if available, otherwise post date.
|
||||
$release_date = get_post_meta( $this->post->ID, '_fedistream_release_date', true );
|
||||
|
||||
if ( $release_date ) {
|
||||
$date = \DateTime::createFromFormat( 'Y-m-d', $release_date );
|
||||
if ( $date ) {
|
||||
return $date->format( 'c' );
|
||||
}
|
||||
}
|
||||
|
||||
return get_the_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the updated date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_updated(): string {
|
||||
return get_the_modified_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total duration.
|
||||
*
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
public function get_duration(): string {
|
||||
$seconds = (int) get_post_meta( $this->post->ID, '_fedistream_total_duration', true );
|
||||
|
||||
if ( ! $seconds ) {
|
||||
// Calculate from tracks.
|
||||
$tracks = $this->get_tracks();
|
||||
foreach ( $tracks as $track ) {
|
||||
$seconds += (int) get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->format_duration_iso8601( $seconds );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tracks in this album.
|
||||
*
|
||||
* @return array Array of WP_Post objects.
|
||||
*/
|
||||
public function get_tracks(): array {
|
||||
$args = array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_key' => '_fedistream_track_number', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'orderby' => 'meta_value_num',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_album_id',
|
||||
'value' => $this->post->ID,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
|
||||
return $query->posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total item count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_total_items(): int {
|
||||
$count = (int) get_post_meta( $this->post->ID, '_fedistream_total_tracks', true );
|
||||
|
||||
if ( ! $count ) {
|
||||
$count = count( $this->get_tracks() );
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection items (track URIs).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_items(): array {
|
||||
$tracks = $this->get_tracks();
|
||||
$items = array();
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$transformer = new TrackTransformer( $track );
|
||||
$items[] = $transformer->to_object();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image/artwork attachment.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_image_attachment(): ?array {
|
||||
$thumbnail_id = get_post_thumbnail_id( $this->post->ID );
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags (genres).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_tags(): array {
|
||||
$tags = array();
|
||||
|
||||
// Get genres.
|
||||
$genres = get_the_terms( $this->post->ID, 'fedistream_genre' );
|
||||
if ( $genres && ! is_wp_error( $genres ) ) {
|
||||
foreach ( $genres as $genre ) {
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#' . sanitize_title( $genre->name ),
|
||||
'href' => get_term_link( $genre ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ActivityPub object array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_object(): array {
|
||||
$object = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => $this->get_type(),
|
||||
'id' => $this->get_id(),
|
||||
'name' => $this->get_name(),
|
||||
'summary' => $this->get_summary(),
|
||||
'content' => $this->get_content(),
|
||||
'url' => $this->get_url(),
|
||||
'attributedTo' => $this->get_attributed_to(),
|
||||
'published' => $this->get_published(),
|
||||
'updated' => $this->get_updated(),
|
||||
'totalItems' => $this->get_total_items(),
|
||||
'items' => $this->get_items(),
|
||||
);
|
||||
|
||||
// Add duration.
|
||||
$duration = $this->get_duration();
|
||||
if ( $duration ) {
|
||||
$object['duration'] = $duration;
|
||||
}
|
||||
|
||||
// Add image.
|
||||
$image = $this->get_image_attachment();
|
||||
if ( $image ) {
|
||||
$object['image'] = $image;
|
||||
}
|
||||
|
||||
// Add tags.
|
||||
$tags = $this->get_tags();
|
||||
if ( ! empty( $tags ) ) {
|
||||
$object['tag'] = $tags;
|
||||
}
|
||||
|
||||
// Add album-specific metadata.
|
||||
$album_type = get_post_meta( $this->post->ID, '_fedistream_album_type', true );
|
||||
if ( $album_type ) {
|
||||
$object['albumType'] = $album_type;
|
||||
}
|
||||
|
||||
$upc = get_post_meta( $this->post->ID, '_fedistream_upc', true );
|
||||
if ( $upc ) {
|
||||
$object['upc'] = $upc;
|
||||
}
|
||||
|
||||
$catalog = get_post_meta( $this->post->ID, '_fedistream_catalog_number', true );
|
||||
if ( $catalog ) {
|
||||
$object['catalogNumber'] = $catalog;
|
||||
}
|
||||
|
||||
$release_date = get_post_meta( $this->post->ID, '_fedistream_release_date', true );
|
||||
if ( $release_date ) {
|
||||
$object['releaseDate'] = $release_date;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Create activity for this album.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_create_activity(): array {
|
||||
$actor = $this->get_attributed_to();
|
||||
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Create',
|
||||
'id' => $this->get_id() . '#activity-create',
|
||||
'actor' => $actor,
|
||||
'published' => $this->get_published(),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor . '/followers' ),
|
||||
'object' => $this->to_object(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Announce activity for this album.
|
||||
*
|
||||
* @param string $actor_uri The actor announcing the album.
|
||||
* @return array
|
||||
*/
|
||||
public function to_announce_activity( string $actor_uri ): array {
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Announce',
|
||||
'id' => $this->get_id() . '#activity-announce-' . time(),
|
||||
'actor' => $actor_uri,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor_uri . '/followers' ),
|
||||
'object' => $this->get_id(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
}
|
||||
614
includes/ActivityPub/ArtistActor.php
Normal file
614
includes/ActivityPub/ArtistActor.php
Normal file
@@ -0,0 +1,614 @@
|
||||
<?php
|
||||
/**
|
||||
* Artist ActivityPub Actor.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an artist as an ActivityPub actor.
|
||||
*/
|
||||
class ArtistActor {
|
||||
|
||||
/**
|
||||
* The artist post.
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
private \WP_Post $artist;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WP_Post $artist The artist post.
|
||||
*/
|
||||
public function __construct( \WP_Post $artist ) {
|
||||
$this->artist = $artist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor ID (URI).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return get_permalink( $this->artist->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor type.
|
||||
*
|
||||
* @return string Person for solo, Group for bands.
|
||||
*/
|
||||
public function get_type(): string {
|
||||
$artist_type = get_post_meta( $this->artist->ID, '_fedistream_artist_type', true );
|
||||
|
||||
// Bands, duos, collectives are Groups.
|
||||
if ( in_array( $artist_type, array( 'band', 'duo', 'collective', 'orchestra' ), true ) ) {
|
||||
return 'Group';
|
||||
}
|
||||
|
||||
return 'Person';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred username.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_preferred_username(): string {
|
||||
return 'artist-' . $this->artist->ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->artist->post_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor summary/bio.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_summary(): string {
|
||||
if ( ! empty( $this->artist->post_excerpt ) ) {
|
||||
return wp_kses_post( $this->artist->post_excerpt );
|
||||
}
|
||||
|
||||
// Use first paragraph of content as summary.
|
||||
$content = wp_strip_all_tags( $this->artist->post_content );
|
||||
$parts = explode( "\n\n", $content, 2 );
|
||||
|
||||
return ! empty( $parts[0] ) ? wp_trim_words( $parts[0], 50 ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actor URL (profile page).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return get_permalink( $this->artist->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inbox URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_inbox(): string {
|
||||
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'inbox';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outbox URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_outbox(): string {
|
||||
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'outbox';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the followers URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_followers(): string {
|
||||
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'followers';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the following URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_following(): string {
|
||||
return trailingslashit( get_permalink( $this->artist->ID ) ) . 'following';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar/icon.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_icon(): ?array {
|
||||
$thumbnail_id = get_post_thumbnail_id( $this->artist->ID );
|
||||
if ( ! $thumbnail_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'thumbnail' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header/banner image.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_image(): ?array {
|
||||
$banner_id = get_post_meta( $this->artist->ID, '_fedistream_artist_banner', true );
|
||||
if ( ! $banner_id ) {
|
||||
// Fall back to featured image at larger size.
|
||||
$banner_id = get_post_thumbnail_id( $this->artist->ID );
|
||||
}
|
||||
|
||||
if ( ! $banner_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $banner_id, 'large' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $banner_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public key.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_public_key(): array {
|
||||
$key = get_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', true );
|
||||
|
||||
if ( ! $key ) {
|
||||
// Generate key pair if not exists.
|
||||
$this->generate_keys();
|
||||
$key = get_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', true );
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $this->get_id() . '#main-key',
|
||||
'owner' => $this->get_id(),
|
||||
'publicKeyPem' => $key,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate RSA key pair for the artist.
|
||||
*
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function generate_keys(): bool {
|
||||
$config = array(
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
);
|
||||
|
||||
$resource = openssl_pkey_new( $config );
|
||||
if ( ! $resource ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Export private key.
|
||||
openssl_pkey_export( $resource, $private_key );
|
||||
|
||||
// Get public key.
|
||||
$details = openssl_pkey_get_details( $resource );
|
||||
$public_key = $details['key'] ?? '';
|
||||
|
||||
if ( ! $private_key || ! $public_key ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_post_meta( $this->artist->ID, '_fedistream_activitypub_private_key', $private_key );
|
||||
update_post_meta( $this->artist->ID, '_fedistream_activitypub_public_key', $public_key );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attachment properties (social links, etc.).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_attachment(): array {
|
||||
$attachments = array();
|
||||
|
||||
// Add website.
|
||||
$website = get_post_meta( $this->artist->ID, '_fedistream_artist_website', true );
|
||||
if ( $website ) {
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => __( 'Website', 'wp-fedistream' ),
|
||||
'value' => sprintf( '<a href="%s" rel="me nofollow noopener" target="_blank">%s</a>', esc_url( $website ), esc_html( $website ) ),
|
||||
);
|
||||
}
|
||||
|
||||
// Add location.
|
||||
$location = get_post_meta( $this->artist->ID, '_fedistream_artist_location', true );
|
||||
if ( $location ) {
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => __( 'Location', 'wp-fedistream' ),
|
||||
'value' => esc_html( $location ),
|
||||
);
|
||||
}
|
||||
|
||||
// Add social links.
|
||||
$social_links = get_post_meta( $this->artist->ID, '_fedistream_artist_social_links', true );
|
||||
if ( is_array( $social_links ) ) {
|
||||
foreach ( $social_links as $link ) {
|
||||
$platform = $link['platform'] ?? '';
|
||||
$url = $link['url'] ?? '';
|
||||
|
||||
if ( $platform && $url ) {
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => esc_html( ucfirst( $platform ) ),
|
||||
'value' => sprintf( '<a href="%s" rel="me nofollow noopener" target="_blank">%s</a>', esc_url( $url ), esc_html( $url ) ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add formed date.
|
||||
$formed = get_post_meta( $this->artist->ID, '_fedistream_artist_formed_date', true );
|
||||
if ( $formed ) {
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => __( 'Active Since', 'wp-fedistream' ),
|
||||
'value' => esc_html( $formed ),
|
||||
);
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full actor object as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
$actor = array(
|
||||
'@context' => array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
array(
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'PropertyValue' => 'schema:PropertyValue',
|
||||
'value' => 'schema:value',
|
||||
),
|
||||
),
|
||||
'id' => $this->get_id(),
|
||||
'type' => $this->get_type(),
|
||||
'preferredUsername' => $this->get_preferred_username(),
|
||||
'name' => $this->get_name(),
|
||||
'summary' => $this->get_summary(),
|
||||
'url' => $this->get_url(),
|
||||
'inbox' => $this->get_inbox(),
|
||||
'outbox' => $this->get_outbox(),
|
||||
'followers' => $this->get_followers(),
|
||||
'following' => $this->get_following(),
|
||||
'publicKey' => $this->get_public_key(),
|
||||
'manuallyApprovesFollowers' => false,
|
||||
'published' => get_the_date( 'c', $this->artist ),
|
||||
'updated' => get_the_modified_date( 'c', $this->artist ),
|
||||
);
|
||||
|
||||
// Add icon if available.
|
||||
$icon = $this->get_icon();
|
||||
if ( $icon ) {
|
||||
$actor['icon'] = $icon;
|
||||
}
|
||||
|
||||
// Add image if available.
|
||||
$image = $this->get_image();
|
||||
if ( $image ) {
|
||||
$actor['image'] = $image;
|
||||
}
|
||||
|
||||
// Add attachments.
|
||||
$attachments = $this->get_attachment();
|
||||
if ( ! empty( $attachments ) ) {
|
||||
$actor['attachment'] = $attachments;
|
||||
}
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get webfinger data for the artist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_webfinger(): array {
|
||||
$host = wp_parse_url( home_url(), PHP_URL_HOST );
|
||||
$resource = 'acct:' . $this->get_preferred_username() . '@' . $host;
|
||||
|
||||
return array(
|
||||
'subject' => $resource,
|
||||
'aliases' => array(
|
||||
$this->get_id(),
|
||||
$this->get_url(),
|
||||
),
|
||||
'links' => array(
|
||||
array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $this->get_id(),
|
||||
),
|
||||
array(
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => $this->get_url(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outbox collection.
|
||||
*
|
||||
* @param int $page The page number (0 for summary).
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public function get_outbox_collection( int $page = 0, int $per_page = 20 ): array {
|
||||
// Get all tracks and albums by this artist.
|
||||
$args = array(
|
||||
'post_type' => array( 'fedistream_track', 'fedistream_album' ),
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => '_fedistream_album_artist',
|
||||
'value' => $this->artist->ID,
|
||||
),
|
||||
array(
|
||||
'key' => '_fedistream_artist_ids',
|
||||
'value' => $this->artist->ID,
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
$total = $query->found_posts;
|
||||
|
||||
// Return summary if page 0.
|
||||
if ( $page === 0 ) {
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->get_outbox(),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $total,
|
||||
'first' => $this->get_outbox() . '?page=1',
|
||||
'last' => $this->get_outbox() . '?page=' . max( 1, ceil( $total / $per_page ) ),
|
||||
);
|
||||
}
|
||||
|
||||
// Get paginated items.
|
||||
$args['posts_per_page'] = $per_page;
|
||||
$args['paged'] = $page;
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'DESC';
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
$items = array();
|
||||
|
||||
foreach ( $query->posts as $post ) {
|
||||
$items[] = $this->post_to_create_activity( $post );
|
||||
}
|
||||
|
||||
$collection = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->get_outbox() . '?page=' . $page,
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'partOf' => $this->get_outbox(),
|
||||
'orderedItems' => $items,
|
||||
);
|
||||
|
||||
// Add prev/next links.
|
||||
if ( $page > 1 ) {
|
||||
$collection['prev'] = $this->get_outbox() . '?page=' . ( $page - 1 );
|
||||
}
|
||||
|
||||
$total_pages = ceil( $total / $per_page );
|
||||
if ( $page < $total_pages ) {
|
||||
$collection['next'] = $this->get_outbox() . '?page=' . ( $page + 1 );
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a post to a Create activity.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
* @return array
|
||||
*/
|
||||
private function post_to_create_activity( \WP_Post $post ): array {
|
||||
$object = array(
|
||||
'type' => 'fedistream_track' === $post->post_type ? 'Audio' : 'Collection',
|
||||
'id' => get_permalink( $post->ID ),
|
||||
'name' => $post->post_title,
|
||||
'content' => wp_strip_all_tags( $post->post_content ),
|
||||
'attributedTo' => $this->get_id(),
|
||||
'published' => get_the_date( 'c', $post ),
|
||||
'url' => get_permalink( $post->ID ),
|
||||
);
|
||||
|
||||
// Add track-specific properties.
|
||||
if ( 'fedistream_track' === $post->post_type ) {
|
||||
$duration = get_post_meta( $post->ID, '_fedistream_duration', true );
|
||||
if ( $duration ) {
|
||||
$object['duration'] = $this->format_duration_iso8601( (int) $duration );
|
||||
}
|
||||
|
||||
$audio_id = get_post_meta( $post->ID, '_fedistream_audio_file', true );
|
||||
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||||
if ( $audio_url ) {
|
||||
$object['attachment'] = array(
|
||||
'type' => 'Audio',
|
||||
'mediaType' => 'audio/mpeg',
|
||||
'url' => $audio_url,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add thumbnail.
|
||||
$thumbnail_id = get_post_thumbnail_id( $post->ID );
|
||||
if ( $thumbnail_id ) {
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
|
||||
if ( $image ) {
|
||||
$object['image'] = array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Create',
|
||||
'id' => get_permalink( $post->ID ) . '#activity-create',
|
||||
'actor' => $this->get_id(),
|
||||
'published' => get_the_date( 'c', $post ),
|
||||
'object' => $object,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the followers collection.
|
||||
*
|
||||
* @param int $page The page number (0 for summary).
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public function get_followers_collection( int $page = 0, int $per_page = 20 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_followers';
|
||||
|
||||
// Get total count.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$total = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE artist_id = %d",
|
||||
$this->artist->ID
|
||||
)
|
||||
);
|
||||
|
||||
// Return summary if page 0.
|
||||
if ( $page === 0 ) {
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->get_followers(),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $total,
|
||||
'first' => $this->get_followers() . '?page=1',
|
||||
);
|
||||
}
|
||||
|
||||
// Get paginated items.
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$followers = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT follower_uri FROM {$table} WHERE artist_id = %d ORDER BY followed_at DESC LIMIT %d OFFSET %d",
|
||||
$this->artist->ID,
|
||||
$per_page,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
$collection = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->get_followers() . '?page=' . $page,
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'partOf' => $this->get_followers(),
|
||||
'orderedItems' => $followers ?: array(),
|
||||
);
|
||||
|
||||
// Add prev/next links.
|
||||
if ( $page > 1 ) {
|
||||
$collection['prev'] = $this->get_followers() . '?page=' . ( $page - 1 );
|
||||
}
|
||||
|
||||
$total_pages = ceil( $total / $per_page );
|
||||
if ( $page < $total_pages ) {
|
||||
$collection['next'] = $this->get_followers() . '?page=' . ( $page + 1 );
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
}
|
||||
477
includes/ActivityPub/FollowerHandler.php
Normal file
477
includes/ActivityPub/FollowerHandler.php
Normal file
@@ -0,0 +1,477 @@
|
||||
<?php
|
||||
/**
|
||||
* Follower Handler for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles ActivityPub follower management.
|
||||
*/
|
||||
class FollowerHandler {
|
||||
|
||||
/**
|
||||
* The table name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $table;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->table = $wpdb->prefix . 'fedistream_followers';
|
||||
|
||||
// Register inbox handlers for follow activities.
|
||||
add_action( 'activitypub_inbox_follow', array( $this, 'handle_follow' ), 10, 2 );
|
||||
add_action( 'activitypub_inbox_undo', array( $this, 'handle_undo' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Follow activity.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The local user ID (if applicable).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_follow( array $activity, int $user_id ): void {
|
||||
$actor = $activity['actor'] ?? '';
|
||||
$object = $activity['object'] ?? '';
|
||||
|
||||
if ( ! $actor || ! $object ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the artist being followed.
|
||||
$artist_id = $this->get_artist_from_object( $object );
|
||||
if ( ! $artist_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add follower.
|
||||
$result = $this->add_follower( $artist_id, $actor, $activity );
|
||||
|
||||
if ( $result ) {
|
||||
// Send Accept activity.
|
||||
$this->send_accept( $artist_id, $activity );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Undo activity (for Unfollow).
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The local user ID (if applicable).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_undo( array $activity, int $user_id ): void {
|
||||
$actor = $activity['actor'] ?? '';
|
||||
$object = $activity['object'] ?? array();
|
||||
|
||||
if ( ! $actor || ! is_array( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is an Undo Follow.
|
||||
$type = $object['type'] ?? '';
|
||||
if ( 'Follow' !== $type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follow_object = $object['object'] ?? '';
|
||||
if ( ! $follow_object ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the artist being unfollowed.
|
||||
$artist_id = $this->get_artist_from_object( $follow_object );
|
||||
if ( ! $artist_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove follower.
|
||||
$this->remove_follower( $artist_id, $actor );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist ID from an object URI.
|
||||
*
|
||||
* @param string $object The object URI.
|
||||
* @return int|null Artist ID or null.
|
||||
*/
|
||||
private function get_artist_from_object( string $object ): ?int {
|
||||
// Try to find by permalink.
|
||||
$post_id = url_to_postid( $object );
|
||||
|
||||
if ( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
if ( $post && 'fedistream_artist' === $post->post_type ) {
|
||||
return $post_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find by artist handle pattern.
|
||||
if ( preg_match( '/artist-(\d+)/', $object, $matches ) ) {
|
||||
$artist_id = absint( $matches[1] );
|
||||
$post = get_post( $artist_id );
|
||||
if ( $post && 'fedistream_artist' === $post->post_type ) {
|
||||
return $artist_id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a follower.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param string $follower_uri The follower's actor URI.
|
||||
* @param array $activity_data The original activity data.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function add_follower( int $artist_id, string $follower_uri, array $activity_data = array() ): bool {
|
||||
global $wpdb;
|
||||
|
||||
// Check if already following.
|
||||
if ( $this->is_following( $artist_id, $follower_uri ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fetch follower info.
|
||||
$follower_info = $this->fetch_actor( $follower_uri );
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$result = $wpdb->insert(
|
||||
$this->table,
|
||||
array(
|
||||
'artist_id' => $artist_id,
|
||||
'follower_uri' => $follower_uri,
|
||||
'follower_name' => $follower_info['name'] ?? '',
|
||||
'follower_icon' => $follower_info['icon']['url'] ?? '',
|
||||
'inbox' => $follower_info['inbox'] ?? '',
|
||||
'shared_inbox' => $follower_info['endpoints']['sharedInbox'] ?? $follower_info['sharedInbox'] ?? '',
|
||||
'activity_data' => wp_json_encode( $activity_data ),
|
||||
'followed_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
// Update follower count.
|
||||
$count = $this->get_follower_count( $artist_id );
|
||||
update_post_meta( $artist_id, '_fedistream_follower_count', $count );
|
||||
|
||||
do_action( 'fedistream_artist_followed', $artist_id, $follower_uri, $follower_info );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a follower.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param string $follower_uri The follower's actor URI.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function remove_follower( int $artist_id, string $follower_uri ): bool {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$result = $wpdb->delete(
|
||||
$this->table,
|
||||
array(
|
||||
'artist_id' => $artist_id,
|
||||
'follower_uri' => $follower_uri,
|
||||
),
|
||||
array( '%d', '%s' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
// Update follower count.
|
||||
$count = $this->get_follower_count( $artist_id );
|
||||
update_post_meta( $artist_id, '_fedistream_follower_count', $count );
|
||||
|
||||
do_action( 'fedistream_artist_unfollowed', $artist_id, $follower_uri );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an actor is following an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param string $follower_uri The follower's actor URI.
|
||||
* @return bool
|
||||
*/
|
||||
public function is_following( int $artist_id, string $follower_uri ): bool {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE artist_id = %d AND follower_uri = %s",
|
||||
$artist_id,
|
||||
$follower_uri
|
||||
)
|
||||
);
|
||||
|
||||
return $exists > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get follower count for an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @return int
|
||||
*/
|
||||
public function get_follower_count( int $artist_id ): int {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$this->table} WHERE artist_id = %d",
|
||||
$artist_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get followers for an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param int $limit Maximum number to return.
|
||||
* @param int $offset Offset for pagination.
|
||||
* @return array
|
||||
*/
|
||||
public function get_followers( int $artist_id, int $limit = 20, int $offset = 0 ): array {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$followers = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$this->table} WHERE artist_id = %d ORDER BY followed_at DESC LIMIT %d OFFSET %d",
|
||||
$artist_id,
|
||||
$limit,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
return $followers ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all follower inboxes for an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @return array Array of inbox URLs, with shared inboxes deduplicated.
|
||||
*/
|
||||
public function get_follower_inboxes( int $artist_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT inbox, shared_inbox FROM {$this->table} WHERE artist_id = %d",
|
||||
$artist_id
|
||||
)
|
||||
);
|
||||
|
||||
$inboxes = array();
|
||||
$shared_inboxes = array();
|
||||
|
||||
foreach ( $results as $row ) {
|
||||
// Prefer shared inbox for efficiency.
|
||||
if ( ! empty( $row->shared_inbox ) ) {
|
||||
$shared_inboxes[ $row->shared_inbox ] = true;
|
||||
} elseif ( ! empty( $row->inbox ) ) {
|
||||
$inboxes[ $row->inbox ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Return unique inboxes (shared inboxes + individual inboxes).
|
||||
return array_merge( array_keys( $shared_inboxes ), array_keys( $inboxes ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Accept activity in response to Follow.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param array $activity The original Follow activity.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function send_accept( int $artist_id, array $activity ): bool {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
$follower_uri = $activity['actor'] ?? '';
|
||||
|
||||
if ( ! $follower_uri ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create Accept activity.
|
||||
$accept = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Accept',
|
||||
'id' => $actor->get_id() . '#accept-' . time(),
|
||||
'actor' => $actor->get_id(),
|
||||
'object' => $activity,
|
||||
);
|
||||
|
||||
// Get follower's inbox.
|
||||
$follower_info = $this->fetch_actor( $follower_uri );
|
||||
$inbox = $follower_info['inbox'] ?? '';
|
||||
|
||||
if ( ! $inbox ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the Accept activity.
|
||||
return $this->send_activity( $accept, $inbox, $artist_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch an actor from a remote server.
|
||||
*
|
||||
* @param string $actor_uri The actor URI.
|
||||
* @return array The actor data.
|
||||
*/
|
||||
private function fetch_actor( string $actor_uri ): array {
|
||||
$response = wp_remote_get(
|
||||
$actor_uri,
|
||||
array(
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json, application/ld+json',
|
||||
),
|
||||
'timeout' => 10,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
return is_array( $data ) ? $data : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an activity to an inbox.
|
||||
*
|
||||
* @param array $activity The activity to send.
|
||||
* @param string $inbox The inbox URL.
|
||||
* @param int $artist_id The artist ID (for signing).
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function send_activity( array $activity, string $inbox, int $artist_id ): bool {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the artist's private key for signing.
|
||||
$private_key = get_post_meta( $artist_id, '_fedistream_activitypub_private_key', true );
|
||||
if ( ! $private_key ) {
|
||||
// Generate keys if not exists.
|
||||
$actor = new ArtistActor( $artist );
|
||||
$actor->get_public_key(); // This triggers key generation.
|
||||
$private_key = get_post_meta( $artist_id, '_fedistream_activitypub_private_key', true );
|
||||
}
|
||||
|
||||
if ( ! $private_key ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = wp_json_encode( $activity );
|
||||
$date = gmdate( 'D, d M Y H:i:s T' );
|
||||
|
||||
// Parse inbox URL.
|
||||
$parsed = wp_parse_url( $inbox );
|
||||
$host = $parsed['host'] ?? '';
|
||||
$path = $parsed['path'] ?? '/';
|
||||
|
||||
// Create signature string.
|
||||
$string_to_sign = "(request-target): post {$path}\n";
|
||||
$string_to_sign .= "host: {$host}\n";
|
||||
$string_to_sign .= "date: {$date}\n";
|
||||
$string_to_sign .= 'digest: SHA-256=' . base64_encode( hash( 'sha256', $body, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
// Sign the string.
|
||||
$signature = '';
|
||||
openssl_sign( $string_to_sign, $signature, $private_key, OPENSSL_ALGO_SHA256 );
|
||||
$signature_b64 = base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
// Build signature header.
|
||||
$actor = new ArtistActor( $artist );
|
||||
$key_id = $actor->get_id() . '#main-key';
|
||||
$signature_header = sprintf(
|
||||
'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"',
|
||||
$key_id,
|
||||
$signature_b64
|
||||
);
|
||||
|
||||
// Send the request.
|
||||
$response = wp_remote_post(
|
||||
$inbox,
|
||||
array(
|
||||
'headers' => array(
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Accept' => 'application/activity+json',
|
||||
'Host' => $host,
|
||||
'Date' => $date,
|
||||
'Digest' => 'SHA-256=' . base64_encode( hash( 'sha256', $body, true ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
'Signature' => $signature_header,
|
||||
),
|
||||
'body' => $body,
|
||||
'timeout' => 30,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// Accept responses: 200, 201, 202 are all valid.
|
||||
return $code >= 200 && $code < 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast an activity to all followers.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param array $activity The activity to broadcast.
|
||||
* @return array Results with inbox => success/failure.
|
||||
*/
|
||||
public function broadcast_activity( int $artist_id, array $activity ): array {
|
||||
$inboxes = $this->get_follower_inboxes( $artist_id );
|
||||
$results = array();
|
||||
|
||||
foreach ( $inboxes as $inbox ) {
|
||||
$results[ $inbox ] = $this->send_activity( $activity, $inbox, $artist_id );
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
480
includes/ActivityPub/Integration.php
Normal file
480
includes/ActivityPub/Integration.php
Normal file
@@ -0,0 +1,480 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub integration main class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main ActivityPub integration class.
|
||||
*
|
||||
* Integrates WP FediStream with the WordPress ActivityPub plugin.
|
||||
*/
|
||||
class Integration {
|
||||
|
||||
/**
|
||||
* Whether the ActivityPub plugin is active.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $activitypub_active = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Check if ActivityPub plugin is available.
|
||||
add_action( 'plugins_loaded', array( $this, 'check_activitypub' ), 5 );
|
||||
|
||||
// Initialize integration after ActivityPub is loaded.
|
||||
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the ActivityPub plugin is active.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check_activitypub(): void {
|
||||
$this->activitypub_active = defined( 'ACTIVITYPUB_PLUGIN_VERSION' )
|
||||
|| class_exists( '\Activitypub\Activitypub' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the ActivityPub integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void {
|
||||
if ( ! $this->activitypub_active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register custom post types for ActivityPub.
|
||||
add_filter( 'activitypub_post_types', array( $this, 'register_post_types' ) );
|
||||
|
||||
// Register custom transformers.
|
||||
add_filter( 'activitypub_transformers', array( $this, 'register_transformers' ) );
|
||||
|
||||
// Register artist actors.
|
||||
add_action( 'init', array( $this, 'register_artist_actors' ), 20 );
|
||||
|
||||
// Add custom properties to ActivityPub objects.
|
||||
add_filter( 'activitypub_activity_object_array', array( $this, 'add_audio_properties' ), 10, 3 );
|
||||
|
||||
// Handle incoming activities.
|
||||
add_action( 'activitypub_inbox_like', array( $this, 'handle_like' ), 10, 2 );
|
||||
add_action( 'activitypub_inbox_announce', array( $this, 'handle_announce' ), 10, 2 );
|
||||
add_action( 'activitypub_inbox_create', array( $this, 'handle_create' ), 10, 2 );
|
||||
|
||||
// Add hooks for publishing.
|
||||
add_action( 'publish_fedistream_track', array( $this, 'on_publish_track' ), 10, 2 );
|
||||
add_action( 'publish_fedistream_album', array( $this, 'on_publish_album' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ActivityPub is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_active(): bool {
|
||||
return $this->activitypub_active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register FediStream post types with ActivityPub.
|
||||
*
|
||||
* @param array $post_types The registered post types.
|
||||
* @return array Modified post types.
|
||||
*/
|
||||
public function register_post_types( array $post_types ): array {
|
||||
$post_types[] = 'fedistream_track';
|
||||
$post_types[] = 'fedistream_album';
|
||||
$post_types[] = 'fedistream_playlist';
|
||||
|
||||
return array_unique( $post_types );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom transformers for FediStream post types.
|
||||
*
|
||||
* @param array $transformers The registered transformers.
|
||||
* @return array Modified transformers.
|
||||
*/
|
||||
public function register_transformers( array $transformers ): array {
|
||||
$transformers['fedistream_track'] = TrackTransformer::class;
|
||||
$transformers['fedistream_album'] = AlbumTransformer::class;
|
||||
$transformers['fedistream_playlist'] = PlaylistTransformer::class;
|
||||
|
||||
return $transformers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register artist actors for ActivityPub.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_artist_actors(): void {
|
||||
// Hook into ActivityPub actor discovery.
|
||||
add_filter( 'activitypub_actor', array( $this, 'get_artist_actor' ), 10, 2 );
|
||||
|
||||
// Add artist webfinger handler.
|
||||
add_filter( 'webfinger_data', array( $this, 'add_artist_webfinger' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist actor for ActivityPub.
|
||||
*
|
||||
* @param mixed $actor The current actor.
|
||||
* @param string $id The actor ID or handle.
|
||||
* @return mixed The actor object or original.
|
||||
*/
|
||||
public function get_artist_actor( $actor, string $id ) {
|
||||
// Check if this is an artist handle.
|
||||
if ( strpos( $id, 'artist-' ) === 0 ) {
|
||||
$artist_id = absint( str_replace( 'artist-', '', $id ) );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( $artist && 'fedistream_artist' === $artist->post_type ) {
|
||||
return new ArtistActor( $artist );
|
||||
}
|
||||
}
|
||||
|
||||
return $actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add artist to webfinger data.
|
||||
*
|
||||
* @param array $data The webfinger data.
|
||||
* @param string $resource The requested resource.
|
||||
* @return array Modified webfinger data.
|
||||
*/
|
||||
public function add_artist_webfinger( array $data, string $resource ): array {
|
||||
// Parse acct: URI.
|
||||
if ( preg_match( '/^acct:artist-(\d+)@/', $resource, $matches ) ) {
|
||||
$artist_id = absint( $matches[1] );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( $artist && 'fedistream_artist' === $artist->post_type ) {
|
||||
$actor = new ArtistActor( $artist );
|
||||
$data = $actor->get_webfinger();
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add audio-specific properties to ActivityPub objects.
|
||||
*
|
||||
* @param array $array The object array.
|
||||
* @param object $object The ActivityPub object.
|
||||
* @param int $post_id The post ID.
|
||||
* @return array Modified array.
|
||||
*/
|
||||
public function add_audio_properties( array $array, $object, int $post_id ): array {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
// Add audio-specific properties for tracks.
|
||||
if ( 'fedistream_track' === $post->post_type ) {
|
||||
$duration = get_post_meta( $post_id, '_fedistream_duration', true );
|
||||
if ( $duration ) {
|
||||
$array['duration'] = $this->format_duration_iso8601( (int) $duration );
|
||||
}
|
||||
|
||||
$audio_id = get_post_meta( $post_id, '_fedistream_audio_file', true );
|
||||
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||||
if ( $audio_url ) {
|
||||
$array['attachment'][] = array(
|
||||
'type' => 'Audio',
|
||||
'mediaType' => 'audio/mpeg',
|
||||
'url' => $audio_url,
|
||||
'name' => $post->post_title,
|
||||
'duration' => $this->format_duration_iso8601( (int) $duration ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Like activity.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The user ID (actor).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_like( array $activity, int $user_id ): void {
|
||||
$object_id = $activity['object'] ?? '';
|
||||
if ( ! $object_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find local post from object ID.
|
||||
$post_id = url_to_postid( $object_id );
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the like.
|
||||
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'like', $activity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Announce (boost) activity.
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The user ID (actor).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_announce( array $activity, int $user_id ): void {
|
||||
$object_id = $activity['object'] ?? '';
|
||||
if ( ! $object_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find local post from object ID.
|
||||
$post_id = url_to_postid( $object_id );
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the boost.
|
||||
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'boost', $activity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming Create activity (comments/replies).
|
||||
*
|
||||
* @param array $activity The activity data.
|
||||
* @param int $user_id The user ID (actor).
|
||||
* @return void
|
||||
*/
|
||||
public function handle_create( array $activity, int $user_id ): void {
|
||||
$object = $activity['object'] ?? array();
|
||||
if ( empty( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a reply to our content.
|
||||
$in_reply_to = $object['inReplyTo'] ?? '';
|
||||
if ( ! $in_reply_to ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find local post from reply target.
|
||||
$post_id = url_to_postid( $in_reply_to );
|
||||
if ( ! $post_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the reply.
|
||||
$this->record_reaction( $post_id, $activity['actor'] ?? '', 'reply', $activity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a reaction from the Fediverse.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param string $actor The actor URI.
|
||||
* @param string $type The reaction type (like, boost, reply).
|
||||
* @param array $activity The full activity data.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
private function record_reaction( int $post_id, string $actor, string $type, array $activity ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_reactions';
|
||||
|
||||
// Check if table exists.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$table_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
'SHOW TABLES LIKE %s',
|
||||
$table
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $table_exists ) {
|
||||
$this->create_reactions_table();
|
||||
}
|
||||
|
||||
// Insert reaction.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$result = $wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'actor_uri' => $actor,
|
||||
'reaction_type' => $type,
|
||||
'activity_data' => wp_json_encode( $activity ),
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s' )
|
||||
);
|
||||
|
||||
// Update reaction count meta.
|
||||
if ( $result ) {
|
||||
$meta_key = '_fedistream_' . $type . '_count';
|
||||
$count = (int) get_post_meta( $post_id, $meta_key, true );
|
||||
update_post_meta( $post_id, $meta_key, $count + 1 );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the reactions table.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function create_reactions_table(): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_reactions';
|
||||
$charset = $wpdb->get_charset_collate();
|
||||
|
||||
$sql = "CREATE TABLE IF NOT EXISTS {$table} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
post_id bigint(20) unsigned NOT NULL,
|
||||
actor_uri varchar(2083) NOT NULL,
|
||||
reaction_type varchar(50) NOT NULL,
|
||||
activity_data longtext,
|
||||
created_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY post_id (post_id),
|
||||
KEY actor_uri (actor_uri(191)),
|
||||
KEY reaction_type (reaction_type)
|
||||
) {$charset};";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle track publishing.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param \WP_Post $post The post object.
|
||||
* @return void
|
||||
*/
|
||||
public function on_publish_track( int $post_id, \WP_Post $post ): void {
|
||||
// The ActivityPub plugin will handle the publishing automatically.
|
||||
// This hook is for any additional custom logic.
|
||||
do_action( 'fedistream_track_published_activitypub', $post_id, $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle album publishing.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param \WP_Post $post The post object.
|
||||
* @return void
|
||||
*/
|
||||
public function on_publish_album( int $post_id, \WP_Post $post ): void {
|
||||
// The ActivityPub plugin will handle the publishing automatically.
|
||||
// This hook is for any additional custom logic.
|
||||
do_action( 'fedistream_album_published_activitypub', $post_id, $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reactions for a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param string $type Optional reaction type filter.
|
||||
* @return array The reactions.
|
||||
*/
|
||||
public function get_reactions( int $post_id, string $type = '' ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_reactions';
|
||||
|
||||
if ( $type ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE post_id = %d AND reaction_type = %s ORDER BY created_at DESC",
|
||||
$post_id,
|
||||
$type
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE post_id = %d ORDER BY created_at DESC",
|
||||
$post_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $results ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reaction counts for a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @return array The reaction counts.
|
||||
*/
|
||||
public function get_reaction_counts( int $post_id ): array {
|
||||
return array(
|
||||
'likes' => (int) get_post_meta( $post_id, '_fedistream_like_count', true ),
|
||||
'boosts' => (int) get_post_meta( $post_id, '_fedistream_boost_count', true ),
|
||||
'replies' => (int) get_post_meta( $post_id, '_fedistream_reply_count', true ),
|
||||
);
|
||||
}
|
||||
}
|
||||
415
includes/ActivityPub/Outbox.php
Normal file
415
includes/ActivityPub/Outbox.php
Normal file
@@ -0,0 +1,415 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox handler for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles publishing activities to followers.
|
||||
*/
|
||||
class Outbox {
|
||||
|
||||
/**
|
||||
* The follower handler.
|
||||
*
|
||||
* @var FollowerHandler
|
||||
*/
|
||||
private FollowerHandler $follower_handler;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->follower_handler = new FollowerHandler();
|
||||
|
||||
// Hook into post publishing.
|
||||
add_action( 'transition_post_status', array( $this, 'on_post_status_change' ), 10, 3 );
|
||||
|
||||
// Hook into post update.
|
||||
add_action( 'post_updated', array( $this, 'on_post_updated' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post status changes.
|
||||
*
|
||||
* @param string $new_status The new post status.
|
||||
* @param string $old_status The old post status.
|
||||
* @param \WP_Post $post The post object.
|
||||
* @return void
|
||||
*/
|
||||
public function on_post_status_change( string $new_status, string $old_status, \WP_Post $post ): void {
|
||||
// Only handle FediStream post types.
|
||||
if ( ! in_array( $post->post_type, array( 'fedistream_track', 'fedistream_album', 'fedistream_playlist' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if publishing is enabled for this post.
|
||||
$enabled = get_post_meta( $post->ID, '_fedistream_activitypub_publish', true );
|
||||
if ( ! $enabled && $enabled !== '' ) {
|
||||
// Default to enabled for new posts.
|
||||
if ( $old_status === 'new' || $old_status === 'auto-draft' ) {
|
||||
update_post_meta( $post->ID, '_fedistream_activitypub_publish', '1' );
|
||||
$enabled = '1';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish on status change to 'publish'.
|
||||
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
|
||||
$this->publish_create( $post );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle post updates.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param \WP_Post $post_after The post object after update.
|
||||
* @param \WP_Post $post_before The post object before update.
|
||||
* @return void
|
||||
*/
|
||||
public function on_post_updated( int $post_id, \WP_Post $post_after, \WP_Post $post_before ): void {
|
||||
// Only handle published FediStream post types.
|
||||
if ( $post_after->post_status !== 'publish' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! in_array( $post_after->post_type, array( 'fedistream_track', 'fedistream_album', 'fedistream_playlist' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already published to ActivityPub.
|
||||
$published = get_post_meta( $post_id, '_fedistream_activitypub_published', true );
|
||||
if ( ! $published ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if update publishing is enabled.
|
||||
$publish_updates = get_post_meta( $post_id, '_fedistream_activitypub_publish_updates', true );
|
||||
if ( ! $publish_updates ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only publish updates for significant changes.
|
||||
if ( $this->has_significant_changes( $post_before, $post_after ) ) {
|
||||
$this->publish_update( $post_after );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post has significant changes.
|
||||
*
|
||||
* @param \WP_Post $before The post before update.
|
||||
* @param \WP_Post $after The post after update.
|
||||
* @return bool
|
||||
*/
|
||||
private function has_significant_changes( \WP_Post $before, \WP_Post $after ): bool {
|
||||
// Check title change.
|
||||
if ( $before->post_title !== $after->post_title ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check content change.
|
||||
if ( $before->post_content !== $after->post_content ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for audio file change (tracks).
|
||||
if ( 'fedistream_track' === $after->post_type ) {
|
||||
$audio_before = get_post_meta( $after->ID, '_fedistream_audio_file_previous', true );
|
||||
$audio_after = get_post_meta( $after->ID, '_fedistream_audio_file', true );
|
||||
if ( $audio_before !== $audio_after ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a Create activity for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post to publish.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function publish_create( \WP_Post $post ): bool {
|
||||
$transformer = $this->get_transformer( $post );
|
||||
if ( ! $transformer ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$activity = $transformer->to_create_activity();
|
||||
|
||||
// Get the artist for this content.
|
||||
$artist_id = $this->get_artist_for_post( $post );
|
||||
if ( ! $artist_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Broadcast to followers.
|
||||
$results = $this->follower_handler->broadcast_activity( $artist_id, $activity );
|
||||
|
||||
// Mark as published.
|
||||
update_post_meta( $post->ID, '_fedistream_activitypub_published', current_time( 'mysql' ) );
|
||||
|
||||
// Log results.
|
||||
$success_count = count( array_filter( $results ) );
|
||||
$total_count = count( $results );
|
||||
|
||||
do_action( 'fedistream_activitypub_published', $post->ID, $success_count, $total_count );
|
||||
|
||||
return $success_count > 0 || $total_count === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an Update activity for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post to update.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function publish_update( \WP_Post $post ): bool {
|
||||
$transformer = $this->get_transformer( $post );
|
||||
if ( ! $transformer ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build Update activity.
|
||||
$object = $transformer->to_object();
|
||||
$actor = $transformer->get_attributed_to();
|
||||
$actor = is_array( $actor ) ? $actor[0] : $actor;
|
||||
|
||||
$activity = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Update',
|
||||
'id' => get_permalink( $post->ID ) . '#activity-update-' . time(),
|
||||
'actor' => $actor,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor . '/followers' ),
|
||||
'object' => $object,
|
||||
);
|
||||
|
||||
// Get the artist for this content.
|
||||
$artist_id = $this->get_artist_for_post( $post );
|
||||
if ( ! $artist_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Broadcast to followers.
|
||||
$results = $this->follower_handler->broadcast_activity( $artist_id, $activity );
|
||||
|
||||
// Update timestamp.
|
||||
update_post_meta( $post->ID, '_fedistream_activitypub_updated', current_time( 'mysql' ) );
|
||||
|
||||
$success_count = count( array_filter( $results ) );
|
||||
|
||||
return $success_count > 0 || count( $results ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an Announce (boost) activity.
|
||||
*
|
||||
* @param int $artist_id The artist ID doing the announcing.
|
||||
* @param string $object_uri The object URI to announce.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function publish_announce( int $artist_id, string $object_uri ): bool {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist || 'fedistream_artist' !== $artist->post_type ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
|
||||
$activity = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Announce',
|
||||
'id' => $actor->get_id() . '#announce-' . time(),
|
||||
'actor' => $actor->get_id(),
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor->get_followers() ),
|
||||
'object' => $object_uri,
|
||||
);
|
||||
|
||||
// Broadcast to followers.
|
||||
$results = $this->follower_handler->broadcast_activity( $artist_id, $activity );
|
||||
|
||||
return count( array_filter( $results ) ) > 0 || count( $results ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a Delete activity for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post being deleted.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function publish_delete( \WP_Post $post ): bool {
|
||||
// Check if was published to ActivityPub.
|
||||
$published = get_post_meta( $post->ID, '_fedistream_activitypub_published', true );
|
||||
if ( ! $published ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actor = $this->get_attributed_to( $post );
|
||||
if ( ! $actor ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$activity = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Delete',
|
||||
'id' => get_permalink( $post->ID ) . '#activity-delete-' . time(),
|
||||
'actor' => $actor,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'object' => array(
|
||||
'type' => 'Tombstone',
|
||||
'id' => get_permalink( $post->ID ),
|
||||
),
|
||||
);
|
||||
|
||||
// Get the artist for this content.
|
||||
$artist_id = $this->get_artist_for_post( $post );
|
||||
if ( ! $artist_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Broadcast to followers.
|
||||
$results = $this->follower_handler->broadcast_activity( $artist_id, $activity );
|
||||
|
||||
return count( array_filter( $results ) ) > 0 || count( $results ) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate transformer for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
* @return TrackTransformer|AlbumTransformer|PlaylistTransformer|null
|
||||
*/
|
||||
private function get_transformer( \WP_Post $post ) {
|
||||
switch ( $post->post_type ) {
|
||||
case 'fedistream_track':
|
||||
return new TrackTransformer( $post );
|
||||
|
||||
case 'fedistream_album':
|
||||
return new AlbumTransformer( $post );
|
||||
|
||||
case 'fedistream_playlist':
|
||||
return new PlaylistTransformer( $post );
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the artist ID associated with a post.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
* @return int|null
|
||||
*/
|
||||
private function get_artist_for_post( \WP_Post $post ): ?int {
|
||||
switch ( $post->post_type ) {
|
||||
case 'fedistream_track':
|
||||
$artist_ids = get_post_meta( $post->ID, '_fedistream_artist_ids', true );
|
||||
if ( is_array( $artist_ids ) && ! empty( $artist_ids ) ) {
|
||||
return absint( $artist_ids[0] );
|
||||
}
|
||||
|
||||
// Fall back to album artist.
|
||||
$album_id = get_post_meta( $post->ID, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
return absint( get_post_meta( $album_id, '_fedistream_album_artist', true ) );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_album':
|
||||
$artist_id = get_post_meta( $post->ID, '_fedistream_album_artist', true );
|
||||
return $artist_id ? absint( $artist_id ) : null;
|
||||
|
||||
case 'fedistream_playlist':
|
||||
// For playlists, try to find an artist owned by the author.
|
||||
$artist_args = array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => 1,
|
||||
'author' => $post->post_author,
|
||||
);
|
||||
|
||||
$artists = get_posts( $artist_args );
|
||||
if ( ! empty( $artists ) ) {
|
||||
return $artists[0]->ID;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributed actor for a post.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
* @return string|null Actor URI.
|
||||
*/
|
||||
private function get_attributed_to( \WP_Post $post ): ?string {
|
||||
$artist_id = $this->get_artist_for_post( $post );
|
||||
|
||||
if ( $artist_id ) {
|
||||
return get_permalink( $artist_id );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger publication for a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @return bool True on success.
|
||||
*/
|
||||
public function manual_publish( int $post_id ): bool {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post || 'publish' !== $post->post_status ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already published.
|
||||
$published = get_post_meta( $post_id, '_fedistream_activitypub_published', true );
|
||||
|
||||
if ( $published ) {
|
||||
return $this->publish_update( $post );
|
||||
}
|
||||
|
||||
return $this->publish_create( $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the outbox collection for an artist.
|
||||
*
|
||||
* @param int $artist_id The artist ID.
|
||||
* @param int $page The page number (0 for summary).
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public function get_collection( int $artist_id, int $page = 0, int $per_page = 20 ): array {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist || 'fedistream_artist' !== $artist->post_type ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
|
||||
return $actor->get_outbox_collection( $page, $per_page );
|
||||
}
|
||||
}
|
||||
433
includes/ActivityPub/PlaylistTransformer.php
Normal file
433
includes/ActivityPub/PlaylistTransformer.php
Normal file
@@ -0,0 +1,433 @@
|
||||
<?php
|
||||
/**
|
||||
* Playlist Transformer for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Playlist posts to ActivityPub OrderedCollection objects.
|
||||
*/
|
||||
class PlaylistTransformer {
|
||||
|
||||
/**
|
||||
* The playlist post.
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
protected \WP_Post $post;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WP_Post $post The playlist post.
|
||||
*/
|
||||
public function __construct( \WP_Post $post ) {
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'OrderedCollection';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object ID (URI).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object name (title).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->post->post_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content (description).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
$content = $this->post->post_content;
|
||||
|
||||
// Apply content filters for proper formatting.
|
||||
$content = apply_filters( 'the_content', $content );
|
||||
|
||||
return wp_kses_post( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summary (excerpt).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_summary(): string {
|
||||
if ( ! empty( $this->post->post_excerpt ) ) {
|
||||
return wp_strip_all_tags( $this->post->post_excerpt );
|
||||
}
|
||||
|
||||
// Generate excerpt from content.
|
||||
return wp_trim_words( wp_strip_all_tags( $this->post->post_content ), 30 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL (permalink).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributed actor (playlist creator).
|
||||
*
|
||||
* @return string User's ActivityPub actor URI.
|
||||
*/
|
||||
public function get_attributed_to(): string {
|
||||
$user_id = $this->post->post_author;
|
||||
|
||||
// Check if there's an artist associated with this user.
|
||||
$artist_args = array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => 1,
|
||||
'author' => $user_id,
|
||||
);
|
||||
|
||||
$artists = get_posts( $artist_args );
|
||||
|
||||
if ( ! empty( $artists ) ) {
|
||||
return get_permalink( $artists[0]->ID );
|
||||
}
|
||||
|
||||
// Fall back to user profile URL.
|
||||
return get_author_posts_url( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the published date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_published(): string {
|
||||
return get_the_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the updated date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_updated(): string {
|
||||
return get_the_modified_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total duration.
|
||||
*
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
public function get_duration(): string {
|
||||
$seconds = (int) get_post_meta( $this->post->ID, '_fedistream_playlist_duration', true );
|
||||
|
||||
if ( ! $seconds ) {
|
||||
// Calculate from tracks.
|
||||
$tracks = $this->get_tracks();
|
||||
foreach ( $tracks as $track ) {
|
||||
$seconds += (int) get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->format_duration_iso8601( $seconds );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tracks in this playlist.
|
||||
*
|
||||
* @return array Array of WP_Post objects.
|
||||
*/
|
||||
public function get_tracks(): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
|
||||
// Get track IDs in order.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$track_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id FROM {$table} WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$this->post->ID
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $track_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Get track posts in order.
|
||||
$args = array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'post__in' => $track_ids,
|
||||
'orderby' => 'post__in',
|
||||
'posts_per_page' => -1,
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
|
||||
return $query->posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total item count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_total_items(): int {
|
||||
$count = (int) get_post_meta( $this->post->ID, '_fedistream_track_count', true );
|
||||
|
||||
if ( ! $count ) {
|
||||
$count = count( $this->get_tracks() );
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ordered items (track objects).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_ordered_items(): array {
|
||||
$tracks = $this->get_tracks();
|
||||
$items = array();
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$transformer = new TrackTransformer( $track );
|
||||
$items[] = $transformer->to_object();
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image/cover attachment.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_image_attachment(): ?array {
|
||||
$thumbnail_id = get_post_thumbnail_id( $this->post->ID );
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags (moods).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_tags(): array {
|
||||
$tags = array();
|
||||
|
||||
// Get moods.
|
||||
$moods = get_the_terms( $this->post->ID, 'fedistream_mood' );
|
||||
if ( $moods && ! is_wp_error( $moods ) ) {
|
||||
foreach ( $moods as $mood ) {
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#' . sanitize_title( $mood->name ),
|
||||
'href' => get_term_link( $mood ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the playlist is public.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_public(): bool {
|
||||
$visibility = get_post_meta( $this->post->ID, '_fedistream_playlist_visibility', true );
|
||||
|
||||
return 'public' === $visibility || empty( $visibility );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ActivityPub object array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_object(): array {
|
||||
$object = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => $this->get_type(),
|
||||
'id' => $this->get_id(),
|
||||
'name' => $this->get_name(),
|
||||
'summary' => $this->get_summary(),
|
||||
'content' => $this->get_content(),
|
||||
'url' => $this->get_url(),
|
||||
'attributedTo' => $this->get_attributed_to(),
|
||||
'published' => $this->get_published(),
|
||||
'updated' => $this->get_updated(),
|
||||
'totalItems' => $this->get_total_items(),
|
||||
'orderedItems' => $this->get_ordered_items(),
|
||||
);
|
||||
|
||||
// Add duration.
|
||||
$duration = $this->get_duration();
|
||||
if ( $duration ) {
|
||||
$object['duration'] = $duration;
|
||||
}
|
||||
|
||||
// Add image.
|
||||
$image = $this->get_image_attachment();
|
||||
if ( $image ) {
|
||||
$object['image'] = $image;
|
||||
}
|
||||
|
||||
// Add tags.
|
||||
$tags = $this->get_tags();
|
||||
if ( ! empty( $tags ) ) {
|
||||
$object['tag'] = $tags;
|
||||
}
|
||||
|
||||
// Add visibility indicator.
|
||||
if ( ! $this->is_public() ) {
|
||||
$object['sensitive'] = false;
|
||||
$visibility = get_post_meta( $this->post->ID, '_fedistream_playlist_visibility', true );
|
||||
if ( 'unlisted' === $visibility ) {
|
||||
$object['visibility'] = 'unlisted';
|
||||
}
|
||||
}
|
||||
|
||||
// Add collaborative flag.
|
||||
$collaborative = get_post_meta( $this->post->ID, '_fedistream_playlist_collaborative', true );
|
||||
if ( $collaborative ) {
|
||||
$object['collaborative'] = true;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Create activity for this playlist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_create_activity(): array {
|
||||
$actor = $this->get_attributed_to();
|
||||
|
||||
// Determine audience based on visibility.
|
||||
$visibility = get_post_meta( $this->post->ID, '_fedistream_playlist_visibility', true );
|
||||
|
||||
$to = array();
|
||||
$cc = array();
|
||||
|
||||
if ( 'public' === $visibility || empty( $visibility ) ) {
|
||||
$to[] = 'https://www.w3.org/ns/activitystreams#Public';
|
||||
$cc[] = $actor . '/followers';
|
||||
} elseif ( 'unlisted' === $visibility ) {
|
||||
$to[] = $actor . '/followers';
|
||||
$cc[] = 'https://www.w3.org/ns/activitystreams#Public';
|
||||
} else {
|
||||
// Private - only followers.
|
||||
$to[] = $actor . '/followers';
|
||||
}
|
||||
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Create',
|
||||
'id' => $this->get_id() . '#activity-create',
|
||||
'actor' => $actor,
|
||||
'published' => $this->get_published(),
|
||||
'to' => $to,
|
||||
'cc' => $cc,
|
||||
'object' => $this->to_object(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Update activity for this playlist.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_update_activity(): array {
|
||||
$actor = $this->get_attributed_to();
|
||||
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Update',
|
||||
'id' => $this->get_id() . '#activity-update-' . time(),
|
||||
'actor' => $actor,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor . '/followers' ),
|
||||
'object' => $this->to_object(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
}
|
||||
476
includes/ActivityPub/RestApi.php
Normal file
476
includes/ActivityPub/RestApi.php
Normal file
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API endpoints for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API handler for ActivityPub endpoints.
|
||||
*/
|
||||
class RestApi {
|
||||
|
||||
/**
|
||||
* The namespace for REST routes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const NAMESPACE = 'fedistream/v1';
|
||||
|
||||
/**
|
||||
* The follower handler.
|
||||
*
|
||||
* @var FollowerHandler
|
||||
*/
|
||||
private FollowerHandler $follower_handler;
|
||||
|
||||
/**
|
||||
* The outbox handler.
|
||||
*
|
||||
* @var Outbox
|
||||
*/
|
||||
private Outbox $outbox;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->follower_handler = new FollowerHandler();
|
||||
$this->outbox = new Outbox();
|
||||
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
|
||||
// Add ActivityPub content type to allowed responses.
|
||||
add_filter( 'rest_pre_serve_request', array( $this, 'serve_activitypub' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes(): void {
|
||||
// Artist actor endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_artist_actor' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist inbox endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/inbox',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'handle_inbox' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist outbox endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/outbox',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_outbox' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
'page' => array(
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Artist followers endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/artist/(?P<id>\d+)/followers',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_followers' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => array( $this, 'validate_artist_id' ),
|
||||
),
|
||||
'page' => array(
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Track/Album/Playlist object endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/object/(?P<type>track|album|playlist)/(?P<id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_object' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'type' => array(
|
||||
'required' => true,
|
||||
),
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Reactions endpoint.
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/reactions/(?P<post_id>\d+)',
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_reactions' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'post_id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
'type' => array(
|
||||
'default' => '',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Manual publish endpoint (requires auth).
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/publish/(?P<post_id>\d+)',
|
||||
array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'manual_publish' ),
|
||||
'permission_callback' => array( $this, 'can_edit_post' ),
|
||||
'args' => array(
|
||||
'post_id' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate artist ID.
|
||||
*
|
||||
* @param mixed $id The ID to validate.
|
||||
* @return bool
|
||||
*/
|
||||
public function validate_artist_id( $id ): bool {
|
||||
$artist = get_post( absint( $id ) );
|
||||
|
||||
return $artist && 'fedistream_artist' === $artist->post_type && 'publish' === $artist->post_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can edit the post.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return bool
|
||||
*/
|
||||
public function can_edit_post( \WP_REST_Request $request ): bool {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
|
||||
return current_user_can( 'edit_post', $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist actor.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_artist_actor( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
|
||||
$response = new \WP_REST_Response( $actor->to_array() );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle inbox requests.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function handle_inbox( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
// Get the activity from request body.
|
||||
$activity = $request->get_json_params();
|
||||
|
||||
if ( empty( $activity ) ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Invalid activity' ), 400 );
|
||||
}
|
||||
|
||||
// Verify HTTP signature (basic verification).
|
||||
$signature = $request->get_header( 'Signature' );
|
||||
if ( ! $signature ) {
|
||||
// Allow unsigned requests for now, but log it.
|
||||
do_action( 'fedistream_unsigned_activity', $activity, $artist_id );
|
||||
}
|
||||
|
||||
// Process the activity based on type.
|
||||
$type = $activity['type'] ?? '';
|
||||
|
||||
switch ( $type ) {
|
||||
case 'Follow':
|
||||
$this->follower_handler->handle_follow( $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Undo':
|
||||
$this->follower_handler->handle_undo( $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Like':
|
||||
do_action( 'activitypub_inbox_like', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Announce':
|
||||
do_action( 'activitypub_inbox_announce', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Create':
|
||||
do_action( 'activitypub_inbox_create', $activity, 0 );
|
||||
break;
|
||||
|
||||
case 'Delete':
|
||||
do_action( 'activitypub_inbox_delete', $activity, 0 );
|
||||
break;
|
||||
|
||||
default:
|
||||
do_action( 'fedistream_inbox_activity', $activity, $artist_id, $type );
|
||||
break;
|
||||
}
|
||||
|
||||
return new \WP_REST_Response( null, 202 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outbox collection.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_outbox( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$page = absint( $request->get_param( 'page' ) );
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
$collection = $actor->get_outbox_collection( $page );
|
||||
|
||||
$response = new \WP_REST_Response( $collection );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get followers collection.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_followers( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$artist_id = absint( $request->get_param( 'id' ) );
|
||||
$page = absint( $request->get_param( 'page' ) );
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Artist not found' ), 404 );
|
||||
}
|
||||
|
||||
$actor = new ArtistActor( $artist );
|
||||
$collection = $actor->get_followers_collection( $page );
|
||||
|
||||
$response = new \WP_REST_Response( $collection );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ActivityPub object.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_object( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$type = $request->get_param( 'type' );
|
||||
$post_id = absint( $request->get_param( 'id' ) );
|
||||
|
||||
$post_type = 'fedistream_' . $type;
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( ! $post || $post->post_type !== $post_type || 'publish' !== $post->post_status ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Object not found' ), 404 );
|
||||
}
|
||||
|
||||
$transformer = null;
|
||||
switch ( $type ) {
|
||||
case 'track':
|
||||
$transformer = new TrackTransformer( $post );
|
||||
break;
|
||||
|
||||
case 'album':
|
||||
$transformer = new AlbumTransformer( $post );
|
||||
break;
|
||||
|
||||
case 'playlist':
|
||||
$transformer = new PlaylistTransformer( $post );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! $transformer ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Invalid object type' ), 400 );
|
||||
}
|
||||
|
||||
$object = $transformer->to_object();
|
||||
$response = new \WP_REST_Response( $object );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reactions for a post.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_reactions( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
$type = sanitize_text_field( $request->get_param( 'type' ) );
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
return new \WP_REST_Response( array( 'error' => 'Post not found' ), 404 );
|
||||
}
|
||||
|
||||
$integration = new Integration();
|
||||
$reactions = $integration->get_reactions( $post_id, $type );
|
||||
$counts = $integration->get_reaction_counts( $post_id );
|
||||
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'reactions' => $reactions,
|
||||
'counts' => $counts,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually publish a post to ActivityPub.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function manual_publish( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$post_id = absint( $request->get_param( 'post_id' ) );
|
||||
|
||||
$result = $this->outbox->manual_publish( $post_id );
|
||||
|
||||
if ( $result ) {
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'success' => true,
|
||||
'message' => __( 'Published to ActivityPub', 'wp-fedistream' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new \WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Failed to publish', 'wp-fedistream' ),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve ActivityPub content type when requested.
|
||||
*
|
||||
* @param bool $served Whether the request has been served.
|
||||
* @param \WP_REST_Response $result The response.
|
||||
* @param \WP_REST_Request $request The request.
|
||||
* @param \WP_REST_Server $server The server.
|
||||
* @return bool
|
||||
*/
|
||||
public function serve_activitypub( bool $served, $result, \WP_REST_Request $request, \WP_REST_Server $server ): bool {
|
||||
// Check if this is a FediStream route.
|
||||
$route = $request->get_route();
|
||||
if ( strpos( $route, '/fedistream/' ) === false ) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
// Check Accept header for ActivityPub.
|
||||
$accept = $request->get_header( 'Accept' );
|
||||
if ( $accept && ( strpos( $accept, 'application/activity+json' ) !== false || strpos( $accept, 'application/ld+json' ) !== false ) ) {
|
||||
// Will be handled by our response headers.
|
||||
}
|
||||
|
||||
return $served;
|
||||
}
|
||||
}
|
||||
412
includes/ActivityPub/TrackTransformer.php
Normal file
412
includes/ActivityPub/TrackTransformer.php
Normal file
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
/**
|
||||
* Track Transformer for ActivityPub.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\ActivityPub;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Track posts to ActivityPub Audio objects.
|
||||
*/
|
||||
class TrackTransformer {
|
||||
|
||||
/**
|
||||
* The track post.
|
||||
*
|
||||
* @var \WP_Post
|
||||
*/
|
||||
protected \WP_Post $post;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \WP_Post $post The track post.
|
||||
*/
|
||||
public function __construct( \WP_Post $post ) {
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'Audio';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object ID (URI).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object name (title).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return $this->post->post_title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content (lyrics or description).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_content(): string {
|
||||
$content = $this->post->post_content;
|
||||
|
||||
// Apply content filters for proper formatting.
|
||||
$content = apply_filters( 'the_content', $content );
|
||||
|
||||
return wp_kses_post( $content );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the summary (excerpt).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_summary(): string {
|
||||
if ( ! empty( $this->post->post_excerpt ) ) {
|
||||
return wp_strip_all_tags( $this->post->post_excerpt );
|
||||
}
|
||||
|
||||
// Generate excerpt from content.
|
||||
return wp_trim_words( wp_strip_all_tags( $this->post->post_content ), 30 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL (permalink).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return get_permalink( $this->post->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributed actor(s).
|
||||
*
|
||||
* @return array|string Artist(s) URIs.
|
||||
*/
|
||||
public function get_attributed_to() {
|
||||
$artist_ids = get_post_meta( $this->post->ID, '_fedistream_artist_ids', true );
|
||||
|
||||
if ( ! is_array( $artist_ids ) || empty( $artist_ids ) ) {
|
||||
// Fall back to album artist.
|
||||
$album_id = get_post_meta( $this->post->ID, '_fedistream_album_id', true );
|
||||
$artist_id = $album_id ? get_post_meta( $album_id, '_fedistream_album_artist', true ) : 0;
|
||||
|
||||
if ( $artist_id ) {
|
||||
return get_permalink( $artist_id );
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
// Return single artist or array of artists.
|
||||
if ( count( $artist_ids ) === 1 ) {
|
||||
return get_permalink( $artist_ids[0] );
|
||||
}
|
||||
|
||||
return array_map( 'get_permalink', $artist_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the published date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_published(): string {
|
||||
return get_the_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the updated date.
|
||||
*
|
||||
* @return string ISO 8601 date.
|
||||
*/
|
||||
public function get_updated(): string {
|
||||
return get_the_modified_date( 'c', $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration.
|
||||
*
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
public function get_duration(): string {
|
||||
$seconds = (int) get_post_meta( $this->post->ID, '_fedistream_duration', true );
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->format_duration_iso8601( $seconds );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the audio attachment.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_audio_attachment(): ?array {
|
||||
$audio_id = get_post_meta( $this->post->ID, '_fedistream_audio_file', true );
|
||||
|
||||
if ( ! $audio_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$audio_url = wp_get_attachment_url( $audio_id );
|
||||
$mime_type = get_post_mime_type( $audio_id );
|
||||
$audio_meta = wp_get_attachment_metadata( $audio_id );
|
||||
|
||||
if ( ! $audio_url ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attachment = array(
|
||||
'type' => 'Audio',
|
||||
'mediaType' => $mime_type ?: 'audio/mpeg',
|
||||
'url' => $audio_url,
|
||||
'name' => $this->post->post_title,
|
||||
);
|
||||
|
||||
// Add duration if available.
|
||||
$duration = $this->get_duration();
|
||||
if ( $duration ) {
|
||||
$attachment['duration'] = $duration;
|
||||
}
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image/thumbnail attachment.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function get_image_attachment(): ?array {
|
||||
$thumbnail_id = get_post_thumbnail_id( $this->post->ID );
|
||||
|
||||
// Fall back to album artwork.
|
||||
if ( ! $thumbnail_id ) {
|
||||
$album_id = get_post_meta( $this->post->ID, '_fedistream_album_id', true );
|
||||
$thumbnail_id = $album_id ? get_post_thumbnail_id( $album_id ) : 0;
|
||||
}
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_src( $thumbnail_id, 'medium' );
|
||||
if ( ! $image ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => get_post_mime_type( $thumbnail_id ),
|
||||
'url' => $image[0],
|
||||
'width' => $image[1],
|
||||
'height' => $image[2],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tags (genres, moods).
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_tags(): array {
|
||||
$tags = array();
|
||||
|
||||
// Get genres.
|
||||
$genres = get_the_terms( $this->post->ID, 'fedistream_genre' );
|
||||
if ( $genres && ! is_wp_error( $genres ) ) {
|
||||
foreach ( $genres as $genre ) {
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#' . sanitize_title( $genre->name ),
|
||||
'href' => get_term_link( $genre ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Get moods.
|
||||
$moods = get_the_terms( $this->post->ID, 'fedistream_mood' );
|
||||
if ( $moods && ! is_wp_error( $moods ) ) {
|
||||
foreach ( $moods as $mood ) {
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#' . sanitize_title( $mood->name ),
|
||||
'href' => get_term_link( $mood ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context (album).
|
||||
*
|
||||
* @return string|null Album URI if available.
|
||||
*/
|
||||
public function get_context(): ?string {
|
||||
$album_id = get_post_meta( $this->post->ID, '_fedistream_album_id', true );
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_permalink( $album_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to ActivityPub object array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_object(): array {
|
||||
$object = array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => $this->get_type(),
|
||||
'id' => $this->get_id(),
|
||||
'name' => $this->get_name(),
|
||||
'summary' => $this->get_summary(),
|
||||
'content' => $this->get_content(),
|
||||
'url' => $this->get_url(),
|
||||
'attributedTo' => $this->get_attributed_to(),
|
||||
'published' => $this->get_published(),
|
||||
'updated' => $this->get_updated(),
|
||||
);
|
||||
|
||||
// Add duration.
|
||||
$duration = $this->get_duration();
|
||||
if ( $duration ) {
|
||||
$object['duration'] = $duration;
|
||||
}
|
||||
|
||||
// Add attachments.
|
||||
$attachments = array();
|
||||
|
||||
$audio = $this->get_audio_attachment();
|
||||
if ( $audio ) {
|
||||
$attachments[] = $audio;
|
||||
}
|
||||
|
||||
$image = $this->get_image_attachment();
|
||||
if ( $image ) {
|
||||
$attachments[] = $image;
|
||||
}
|
||||
|
||||
if ( ! empty( $attachments ) ) {
|
||||
$object['attachment'] = $attachments;
|
||||
}
|
||||
|
||||
// Add tags.
|
||||
$tags = $this->get_tags();
|
||||
if ( ! empty( $tags ) ) {
|
||||
$object['tag'] = $tags;
|
||||
}
|
||||
|
||||
// Add context (album).
|
||||
$context = $this->get_context();
|
||||
if ( $context ) {
|
||||
$object['context'] = $context;
|
||||
}
|
||||
|
||||
// Add additional metadata.
|
||||
$explicit = get_post_meta( $this->post->ID, '_fedistream_explicit', true );
|
||||
if ( $explicit ) {
|
||||
$object['sensitive'] = true;
|
||||
}
|
||||
|
||||
// Add ISRC if available.
|
||||
$isrc = get_post_meta( $this->post->ID, '_fedistream_isrc', true );
|
||||
if ( $isrc ) {
|
||||
$object['isrc'] = $isrc;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Create activity for this track.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_create_activity(): array {
|
||||
$attributed_to = $this->get_attributed_to();
|
||||
$actor = is_array( $attributed_to ) ? $attributed_to[0] : $attributed_to;
|
||||
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Create',
|
||||
'id' => $this->get_id() . '#activity-create',
|
||||
'actor' => $actor,
|
||||
'published' => $this->get_published(),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor . '/followers' ),
|
||||
'object' => $this->to_object(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Announce activity for this track.
|
||||
*
|
||||
* @param string $actor_uri The actor announcing the track.
|
||||
* @return array
|
||||
*/
|
||||
public function to_announce_activity( string $actor_uri ): array {
|
||||
return array(
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Announce',
|
||||
'id' => $this->get_id() . '#activity-announce-' . time(),
|
||||
'actor' => $actor_uri,
|
||||
'published' => gmdate( 'c' ),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( $actor_uri . '/followers' ),
|
||||
'object' => $this->get_id(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration as ISO 8601.
|
||||
*
|
||||
* @param int $seconds The duration in seconds.
|
||||
* @return string ISO 8601 duration.
|
||||
*/
|
||||
private function format_duration_iso8601( int $seconds ): string {
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$minutes = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
$duration = 'PT';
|
||||
if ( $hours > 0 ) {
|
||||
$duration .= $hours . 'H';
|
||||
}
|
||||
if ( $minutes > 0 ) {
|
||||
$duration .= $minutes . 'M';
|
||||
}
|
||||
if ( $secs > 0 || ( $hours === 0 && $minutes === 0 ) ) {
|
||||
$duration .= $secs . 'S';
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
}
|
||||
604
includes/Admin/ListColumns.php
Normal file
604
includes/Admin/ListColumns.php
Normal file
@@ -0,0 +1,604 @@
|
||||
<?php
|
||||
/**
|
||||
* Custom list table columns for admin.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Admin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ListColumns class.
|
||||
*
|
||||
* Handles custom columns in admin list tables for all post types.
|
||||
*/
|
||||
class ListColumns {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Artist columns.
|
||||
add_filter( 'manage_fedistream_artist_posts_columns', array( $this, 'artist_columns' ) );
|
||||
add_action( 'manage_fedistream_artist_posts_custom_column', array( $this, 'artist_column_content' ), 10, 2 );
|
||||
add_filter( 'manage_edit-fedistream_artist_sortable_columns', array( $this, 'artist_sortable_columns' ) );
|
||||
|
||||
// Album columns.
|
||||
add_filter( 'manage_fedistream_album_posts_columns', array( $this, 'album_columns' ) );
|
||||
add_action( 'manage_fedistream_album_posts_custom_column', array( $this, 'album_column_content' ), 10, 2 );
|
||||
add_filter( 'manage_edit-fedistream_album_sortable_columns', array( $this, 'album_sortable_columns' ) );
|
||||
|
||||
// Track columns.
|
||||
add_filter( 'manage_fedistream_track_posts_columns', array( $this, 'track_columns' ) );
|
||||
add_action( 'manage_fedistream_track_posts_custom_column', array( $this, 'track_column_content' ), 10, 2 );
|
||||
add_filter( 'manage_edit-fedistream_track_sortable_columns', array( $this, 'track_sortable_columns' ) );
|
||||
|
||||
// Playlist columns.
|
||||
add_filter( 'manage_fedistream_playlist_posts_columns', array( $this, 'playlist_columns' ) );
|
||||
add_action( 'manage_fedistream_playlist_posts_custom_column', array( $this, 'playlist_column_content' ), 10, 2 );
|
||||
add_filter( 'manage_edit-fedistream_playlist_sortable_columns', array( $this, 'playlist_sortable_columns' ) );
|
||||
|
||||
// Handle sorting.
|
||||
add_action( 'pre_get_posts', array( $this, 'handle_sorting' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Define artist list columns.
|
||||
*
|
||||
* @param array $columns Default columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function artist_columns( array $columns ): array {
|
||||
$new_columns = array();
|
||||
|
||||
foreach ( $columns as $key => $value ) {
|
||||
if ( 'title' === $key ) {
|
||||
$new_columns['fedistream_photo'] = '';
|
||||
}
|
||||
$new_columns[ $key ] = $value;
|
||||
|
||||
if ( 'title' === $key ) {
|
||||
$new_columns['fedistream_type'] = __( 'Type', 'wp-fedistream' );
|
||||
$new_columns['fedistream_albums'] = __( 'Albums', 'wp-fedistream' );
|
||||
$new_columns['fedistream_tracks'] = __( 'Tracks', 'wp-fedistream' );
|
||||
}
|
||||
}
|
||||
|
||||
// Remove date, we'll add it back at the end.
|
||||
unset( $new_columns['date'] );
|
||||
$new_columns['date'] = __( 'Date', 'wp-fedistream' );
|
||||
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artist column content.
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function artist_column_content( string $column, int $post_id ): void {
|
||||
switch ( $column ) {
|
||||
case 'fedistream_photo':
|
||||
$thumbnail = get_the_post_thumbnail( $post_id, array( 40, 40 ) );
|
||||
if ( $thumbnail ) {
|
||||
echo wp_kses_post( $thumbnail );
|
||||
} else {
|
||||
echo '<span class="dashicons dashicons-admin-users" style="font-size: 40px; width: 40px; height: 40px; color: #ccc;"></span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_type':
|
||||
$type = get_post_meta( $post_id, '_fedistream_artist_type', true );
|
||||
$types = array(
|
||||
'solo' => __( 'Solo', 'wp-fedistream' ),
|
||||
'band' => __( 'Band', 'wp-fedistream' ),
|
||||
'duo' => __( 'Duo', 'wp-fedistream' ),
|
||||
'collective' => __( 'Collective', 'wp-fedistream' ),
|
||||
);
|
||||
echo esc_html( $types[ $type ] ?? __( 'Solo', 'wp-fedistream' ) );
|
||||
break;
|
||||
|
||||
case 'fedistream_albums':
|
||||
$count = $this->count_posts_by_meta( 'fedistream_album', '_fedistream_album_artist', $post_id );
|
||||
echo '<a href="' . esc_url( admin_url( 'edit.php?post_type=fedistream_album&artist=' . $post_id ) ) . '">' . esc_html( $count ) . '</a>';
|
||||
break;
|
||||
|
||||
case 'fedistream_tracks':
|
||||
$count = $this->count_tracks_by_artist( $post_id );
|
||||
echo esc_html( $count );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define sortable artist columns.
|
||||
*
|
||||
* @param array $columns Sortable columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function artist_sortable_columns( array $columns ): array {
|
||||
$columns['fedistream_type'] = 'fedistream_type';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define album list columns.
|
||||
*
|
||||
* @param array $columns Default columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function album_columns( array $columns ): array {
|
||||
$new_columns = array();
|
||||
|
||||
foreach ( $columns as $key => $value ) {
|
||||
if ( 'title' === $key ) {
|
||||
$new_columns['fedistream_artwork'] = '';
|
||||
}
|
||||
$new_columns[ $key ] = $value;
|
||||
|
||||
if ( 'title' === $key ) {
|
||||
$new_columns['fedistream_artist'] = __( 'Artist', 'wp-fedistream' );
|
||||
$new_columns['fedistream_type'] = __( 'Type', 'wp-fedistream' );
|
||||
$new_columns['fedistream_tracks'] = __( 'Tracks', 'wp-fedistream' );
|
||||
$new_columns['fedistream_release_date'] = __( 'Release Date', 'wp-fedistream' );
|
||||
}
|
||||
}
|
||||
|
||||
unset( $new_columns['date'] );
|
||||
$new_columns['date'] = __( 'Date', 'wp-fedistream' );
|
||||
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album column content.
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function album_column_content( string $column, int $post_id ): void {
|
||||
switch ( $column ) {
|
||||
case 'fedistream_artwork':
|
||||
$thumbnail = get_the_post_thumbnail( $post_id, array( 40, 40 ) );
|
||||
if ( $thumbnail ) {
|
||||
echo wp_kses_post( $thumbnail );
|
||||
} else {
|
||||
echo '<span class="dashicons dashicons-album" style="font-size: 40px; width: 40px; height: 40px; color: #ccc;"></span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_artist':
|
||||
$artist_id = get_post_meta( $post_id, '_fedistream_album_artist', true );
|
||||
if ( $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist ) {
|
||||
echo '<a href="' . esc_url( get_edit_post_link( $artist_id ) ) . '">' . esc_html( $artist->post_title ) . '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="description">' . esc_html__( 'No artist', 'wp-fedistream' ) . '</span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_type':
|
||||
$type = get_post_meta( $post_id, '_fedistream_album_type', true );
|
||||
$types = array(
|
||||
'album' => __( 'Album', 'wp-fedistream' ),
|
||||
'ep' => __( 'EP', 'wp-fedistream' ),
|
||||
'single' => __( 'Single', 'wp-fedistream' ),
|
||||
'compilation' => __( 'Compilation', 'wp-fedistream' ),
|
||||
'live' => __( 'Live', 'wp-fedistream' ),
|
||||
'remix' => __( 'Remix', 'wp-fedistream' ),
|
||||
);
|
||||
echo esc_html( $types[ $type ] ?? __( 'Album', 'wp-fedistream' ) );
|
||||
break;
|
||||
|
||||
case 'fedistream_tracks':
|
||||
$count = get_post_meta( $post_id, '_fedistream_album_total_tracks', true );
|
||||
echo '<a href="' . esc_url( admin_url( 'edit.php?post_type=fedistream_track&album=' . $post_id ) ) . '">' . esc_html( $count ?: 0 ) . '</a>';
|
||||
break;
|
||||
|
||||
case 'fedistream_release_date':
|
||||
$date = get_post_meta( $post_id, '_fedistream_album_release_date', true );
|
||||
if ( $date ) {
|
||||
echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $date ) ) );
|
||||
} else {
|
||||
echo '<span class="description">—</span>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define sortable album columns.
|
||||
*
|
||||
* @param array $columns Sortable columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function album_sortable_columns( array $columns ): array {
|
||||
$columns['fedistream_type'] = 'fedistream_type';
|
||||
$columns['fedistream_release_date'] = 'fedistream_release_date';
|
||||
$columns['fedistream_artist'] = 'fedistream_artist';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define track list columns.
|
||||
*
|
||||
* @param array $columns Default columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function track_columns( array $columns ): array {
|
||||
$new_columns = array();
|
||||
|
||||
foreach ( $columns as $key => $value ) {
|
||||
if ( 'title' === $key ) {
|
||||
$new_columns['fedistream_artwork'] = '';
|
||||
}
|
||||
$new_columns[ $key ] = $value;
|
||||
|
||||
if ( 'title' === $key ) {
|
||||
$new_columns['fedistream_artists'] = __( 'Artists', 'wp-fedistream' );
|
||||
$new_columns['fedistream_album'] = __( 'Album', 'wp-fedistream' );
|
||||
$new_columns['fedistream_duration'] = __( 'Duration', 'wp-fedistream' );
|
||||
$new_columns['fedistream_plays'] = __( 'Plays', 'wp-fedistream' );
|
||||
}
|
||||
}
|
||||
|
||||
unset( $new_columns['date'] );
|
||||
$new_columns['date'] = __( 'Date', 'wp-fedistream' );
|
||||
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track column content.
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function track_column_content( string $column, int $post_id ): void {
|
||||
switch ( $column ) {
|
||||
case 'fedistream_artwork':
|
||||
$thumbnail = get_the_post_thumbnail( $post_id, array( 40, 40 ) );
|
||||
if ( ! $thumbnail ) {
|
||||
// Try album artwork.
|
||||
$album_id = get_post_meta( $post_id, '_fedistream_track_album', true );
|
||||
if ( $album_id ) {
|
||||
$thumbnail = get_the_post_thumbnail( $album_id, array( 40, 40 ) );
|
||||
}
|
||||
}
|
||||
if ( $thumbnail ) {
|
||||
echo wp_kses_post( $thumbnail );
|
||||
} else {
|
||||
echo '<span class="dashicons dashicons-format-audio" style="font-size: 40px; width: 40px; height: 40px; color: #ccc;"></span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_artists':
|
||||
$artists = get_post_meta( $post_id, '_fedistream_track_artists', true );
|
||||
if ( is_array( $artists ) && ! empty( $artists ) ) {
|
||||
$artist_links = array();
|
||||
foreach ( $artists as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist ) {
|
||||
$artist_links[] = '<a href="' . esc_url( get_edit_post_link( $artist_id ) ) . '">' . esc_html( $artist->post_title ) . '</a>';
|
||||
}
|
||||
}
|
||||
echo wp_kses_post( implode( ', ', $artist_links ) );
|
||||
} else {
|
||||
echo '<span class="description">' . esc_html__( 'No artist', 'wp-fedistream' ) . '</span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_album':
|
||||
$album_id = get_post_meta( $post_id, '_fedistream_track_album', true );
|
||||
if ( $album_id ) {
|
||||
$album = get_post( $album_id );
|
||||
if ( $album ) {
|
||||
echo '<a href="' . esc_url( get_edit_post_link( $album_id ) ) . '">' . esc_html( $album->post_title ) . '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="description">' . esc_html__( 'Single', 'wp-fedistream' ) . '</span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_duration':
|
||||
$duration = get_post_meta( $post_id, '_fedistream_track_duration', true );
|
||||
if ( $duration ) {
|
||||
$minutes = floor( $duration / 60 );
|
||||
$seconds = $duration % 60;
|
||||
echo esc_html( sprintf( '%d:%02d', $minutes, $seconds ) );
|
||||
} else {
|
||||
echo '<span class="description">—</span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_plays':
|
||||
$plays = $this->get_track_plays( $post_id );
|
||||
echo esc_html( number_format_i18n( $plays ) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define sortable track columns.
|
||||
*
|
||||
* @param array $columns Sortable columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function track_sortable_columns( array $columns ): array {
|
||||
$columns['fedistream_duration'] = 'fedistream_duration';
|
||||
$columns['fedistream_plays'] = 'fedistream_plays';
|
||||
$columns['fedistream_album'] = 'fedistream_album';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define playlist list columns.
|
||||
*
|
||||
* @param array $columns Default columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function playlist_columns( array $columns ): array {
|
||||
$new_columns = array();
|
||||
|
||||
foreach ( $columns as $key => $value ) {
|
||||
if ( 'title' === $key ) {
|
||||
$new_columns['fedistream_cover'] = '';
|
||||
}
|
||||
$new_columns[ $key ] = $value;
|
||||
|
||||
if ( 'title' === $key ) {
|
||||
$new_columns['fedistream_tracks'] = __( 'Tracks', 'wp-fedistream' );
|
||||
$new_columns['fedistream_duration'] = __( 'Duration', 'wp-fedistream' );
|
||||
$new_columns['fedistream_visibility'] = __( 'Visibility', 'wp-fedistream' );
|
||||
}
|
||||
}
|
||||
|
||||
unset( $new_columns['date'] );
|
||||
$new_columns['date'] = __( 'Date', 'wp-fedistream' );
|
||||
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist column content.
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function playlist_column_content( string $column, int $post_id ): void {
|
||||
switch ( $column ) {
|
||||
case 'fedistream_cover':
|
||||
$thumbnail = get_the_post_thumbnail( $post_id, array( 40, 40 ) );
|
||||
if ( $thumbnail ) {
|
||||
echo wp_kses_post( $thumbnail );
|
||||
} else {
|
||||
echo '<span class="dashicons dashicons-playlist-audio" style="font-size: 40px; width: 40px; height: 40px; color: #ccc;"></span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_tracks':
|
||||
$count = get_post_meta( $post_id, '_fedistream_playlist_track_count', true );
|
||||
echo esc_html( $count ?: 0 );
|
||||
break;
|
||||
|
||||
case 'fedistream_duration':
|
||||
$duration = get_post_meta( $post_id, '_fedistream_playlist_total_duration', true );
|
||||
if ( $duration ) {
|
||||
if ( $duration >= 3600 ) {
|
||||
$hours = floor( $duration / 3600 );
|
||||
$minutes = floor( ( $duration % 3600 ) / 60 );
|
||||
echo esc_html( sprintf( '%d:%02d:%02d', $hours, $minutes, $duration % 60 ) );
|
||||
} else {
|
||||
$minutes = floor( $duration / 60 );
|
||||
$seconds = $duration % 60;
|
||||
echo esc_html( sprintf( '%d:%02d', $minutes, $seconds ) );
|
||||
}
|
||||
} else {
|
||||
echo '<span class="description">—</span>';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fedistream_visibility':
|
||||
$visibility = get_post_meta( $post_id, '_fedistream_playlist_visibility', true ) ?: 'public';
|
||||
$labels = array(
|
||||
'public' => __( 'Public', 'wp-fedistream' ),
|
||||
'unlisted' => __( 'Unlisted', 'wp-fedistream' ),
|
||||
'private' => __( 'Private', 'wp-fedistream' ),
|
||||
);
|
||||
$icons = array(
|
||||
'public' => 'dashicons-visibility',
|
||||
'unlisted' => 'dashicons-hidden',
|
||||
'private' => 'dashicons-lock',
|
||||
);
|
||||
echo '<span class="dashicons ' . esc_attr( $icons[ $visibility ] ?? 'dashicons-visibility' ) . '" title="' . esc_attr( $labels[ $visibility ] ?? '' ) . '"></span> ';
|
||||
echo esc_html( $labels[ $visibility ] ?? __( 'Public', 'wp-fedistream' ) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define sortable playlist columns.
|
||||
*
|
||||
* @param array $columns Sortable columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function playlist_sortable_columns( array $columns ): array {
|
||||
$columns['fedistream_tracks'] = 'fedistream_track_count';
|
||||
$columns['fedistream_duration'] = 'fedistream_duration';
|
||||
$columns['fedistream_visibility'] = 'fedistream_visibility';
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle custom column sorting.
|
||||
*
|
||||
* @param \WP_Query $query The query object.
|
||||
* @return void
|
||||
*/
|
||||
public function handle_sorting( \WP_Query $query ): void {
|
||||
if ( ! is_admin() || ! $query->is_main_query() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$orderby = $query->get( 'orderby' );
|
||||
|
||||
switch ( $orderby ) {
|
||||
case 'fedistream_type':
|
||||
$post_type = $query->get( 'post_type' );
|
||||
if ( 'fedistream_artist' === $post_type ) {
|
||||
$query->set( 'meta_key', '_fedistream_artist_type' );
|
||||
} elseif ( 'fedistream_album' === $post_type ) {
|
||||
$query->set( 'meta_key', '_fedistream_album_type' );
|
||||
}
|
||||
$query->set( 'orderby', 'meta_value' );
|
||||
break;
|
||||
|
||||
case 'fedistream_release_date':
|
||||
$query->set( 'meta_key', '_fedistream_album_release_date' );
|
||||
$query->set( 'orderby', 'meta_value' );
|
||||
break;
|
||||
|
||||
case 'fedistream_artist':
|
||||
$query->set( 'meta_key', '_fedistream_album_artist' );
|
||||
$query->set( 'orderby', 'meta_value_num' );
|
||||
break;
|
||||
|
||||
case 'fedistream_duration':
|
||||
$post_type = $query->get( 'post_type' );
|
||||
if ( 'fedistream_track' === $post_type ) {
|
||||
$query->set( 'meta_key', '_fedistream_track_duration' );
|
||||
} elseif ( 'fedistream_playlist' === $post_type ) {
|
||||
$query->set( 'meta_key', '_fedistream_playlist_total_duration' );
|
||||
}
|
||||
$query->set( 'orderby', 'meta_value_num' );
|
||||
break;
|
||||
|
||||
case 'fedistream_track_count':
|
||||
$query->set( 'meta_key', '_fedistream_playlist_track_count' );
|
||||
$query->set( 'orderby', 'meta_value_num' );
|
||||
break;
|
||||
|
||||
case 'fedistream_visibility':
|
||||
$query->set( 'meta_key', '_fedistream_playlist_visibility' );
|
||||
$query->set( 'orderby', 'meta_value' );
|
||||
break;
|
||||
|
||||
case 'fedistream_album':
|
||||
$query->set( 'meta_key', '_fedistream_track_album' );
|
||||
$query->set( 'orderby', 'meta_value_num' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Count posts by meta value.
|
||||
*
|
||||
* @param string $post_type Post type.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param mixed $meta_value Meta value.
|
||||
* @return int Count.
|
||||
*/
|
||||
private function count_posts_by_meta( string $post_type, string $meta_key, $meta_value ): int {
|
||||
$query = new \WP_Query(
|
||||
array(
|
||||
'post_type' => $post_type,
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => $meta_key,
|
||||
'meta_value' => $meta_value,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
return $query->found_posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count tracks by artist.
|
||||
*
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return int Track count.
|
||||
*/
|
||||
private function count_tracks_by_artist( int $artist_id ): int {
|
||||
global $wpdb;
|
||||
|
||||
// Count tracks where artist is in the artists array.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $wpdb->posts p
|
||||
INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id
|
||||
WHERE p.post_type = 'fedistream_track'
|
||||
AND p.post_status = 'publish'
|
||||
AND pm.meta_key = '_fedistream_track_artists'
|
||||
AND pm.meta_value LIKE %s",
|
||||
'%"' . $artist_id . '"%'
|
||||
)
|
||||
);
|
||||
|
||||
// Also check serialized format.
|
||||
if ( ! $count ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $wpdb->posts p
|
||||
INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id
|
||||
WHERE p.post_type = 'fedistream_track'
|
||||
AND p.post_status = 'publish'
|
||||
AND pm.meta_key = '_fedistream_track_artists'
|
||||
AND pm.meta_value LIKE %s",
|
||||
'%i:' . $artist_id . ';%'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track play count.
|
||||
*
|
||||
* @param int $track_id Track post ID.
|
||||
* @return int Play count.
|
||||
*/
|
||||
private function get_track_plays( int $track_id ): int {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_plays';
|
||||
|
||||
// Check if table exists.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$table_exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
'SHOW TABLES LIKE %s',
|
||||
$table
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $table_exists ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $table WHERE track_id = %d",
|
||||
$track_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
}
|
||||
1
includes/Admin/index.php
Normal file
1
includes/Admin/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
172
includes/Frontend/Ajax.php
Normal file
172
includes/Frontend/Ajax.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* AJAX handlers for frontend functionality.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles AJAX requests for the frontend.
|
||||
*/
|
||||
class Ajax {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Track data endpoint (public).
|
||||
add_action( 'wp_ajax_fedistream_get_track', array( $this, 'get_track' ) );
|
||||
add_action( 'wp_ajax_nopriv_fedistream_get_track', array( $this, 'get_track' ) );
|
||||
|
||||
// Record play endpoint (public).
|
||||
add_action( 'wp_ajax_fedistream_record_play', array( $this, 'record_play' ) );
|
||||
add_action( 'wp_ajax_nopriv_fedistream_record_play', array( $this, 'record_play' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track data via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function get_track(): void {
|
||||
// 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' ) ) );
|
||||
}
|
||||
|
||||
$track_id = isset( $_POST['track_id'] ) ? absint( $_POST['track_id'] ) : 0;
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid track ID.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Track not found.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
// Check if track is published.
|
||||
if ( 'publish' !== $track->post_status ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Track not available.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
// Get audio file.
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||||
|
||||
if ( ! $audio_url ) {
|
||||
wp_send_json_error( array( 'message' => __( 'No audio file available.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
// Get track metadata.
|
||||
$thumbnail_id = get_post_thumbnail_id( $track_id );
|
||||
$thumbnail = $thumbnail_id ? wp_get_attachment_image_url( $thumbnail_id, 'medium' ) : '';
|
||||
|
||||
// Get album info.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$album = $album_id ? get_post( $album_id ) : null;
|
||||
|
||||
// Get artists.
|
||||
$artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true );
|
||||
$artists = array();
|
||||
|
||||
if ( is_array( $artist_ids ) && ! empty( $artist_ids ) ) {
|
||||
foreach ( $artist_ids as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist && 'fedistream_artist' === $artist->post_type ) {
|
||||
$artists[] = array(
|
||||
'id' => $artist->ID,
|
||||
'name' => $artist->post_title,
|
||||
'link' => get_permalink( $artist->ID ),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get duration.
|
||||
$duration = get_post_meta( $track_id, '_fedistream_duration', true );
|
||||
$duration_formatted = '';
|
||||
if ( $duration ) {
|
||||
$mins = floor( $duration / 60 );
|
||||
$secs = $duration % 60;
|
||||
$duration_formatted = $mins . ':' . str_pad( $secs, 2, '0', STR_PAD_LEFT );
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'id' => $track->ID,
|
||||
'title' => $track->post_title,
|
||||
'permalink' => get_permalink( $track->ID ),
|
||||
'audio_url' => $audio_url,
|
||||
'thumbnail' => $thumbnail,
|
||||
'artists' => $artists,
|
||||
'artist' => ! empty( $artists ) ? $artists[0]['name'] : '',
|
||||
'album' => $album ? $album->post_title : '',
|
||||
'album_id' => $album ? $album->ID : 0,
|
||||
'album_link' => $album ? get_permalink( $album->ID ) : '',
|
||||
'duration' => $duration,
|
||||
'duration_formatted' => $duration_formatted,
|
||||
'explicit' => (bool) get_post_meta( $track_id, '_fedistream_explicit', true ),
|
||||
);
|
||||
|
||||
wp_send_json_success( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a track play via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function record_play(): void {
|
||||
// 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' ) ) );
|
||||
}
|
||||
|
||||
$track_id = isset( $_POST['track_id'] ) ? absint( $_POST['track_id'] ) : 0;
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid track ID.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Track not found.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Insert play record.
|
||||
$table = $wpdb->prefix . 'fedistream_plays';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'track_id' => $track_id,
|
||||
'user_id' => get_current_user_id() ?: null,
|
||||
'played_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%d', '%s' )
|
||||
);
|
||||
|
||||
// Update play count in post meta.
|
||||
$play_count = (int) get_post_meta( $track_id, '_fedistream_play_count', true );
|
||||
update_post_meta( $track_id, '_fedistream_play_count', $play_count + 1 );
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => __( 'Play recorded.', 'wp-fedistream' ),
|
||||
'play_count' => $play_count + 1,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
513
includes/Frontend/Shortcodes.php
Normal file
513
includes/Frontend/Shortcodes.php
Normal file
@@ -0,0 +1,513 @@
|
||||
<?php
|
||||
/**
|
||||
* Shortcodes handler.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend;
|
||||
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers and handles all plugin shortcodes.
|
||||
*/
|
||||
class Shortcodes {
|
||||
|
||||
/**
|
||||
* Plugin instance.
|
||||
*
|
||||
* @var Plugin
|
||||
*/
|
||||
private Plugin $plugin;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->plugin = Plugin::get_instance();
|
||||
$this->register_shortcodes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all shortcodes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function register_shortcodes(): void {
|
||||
add_shortcode( 'fedistream_artist', array( $this, 'render_artist' ) );
|
||||
add_shortcode( 'fedistream_album', array( $this, 'render_album' ) );
|
||||
add_shortcode( 'fedistream_track', array( $this, 'render_track' ) );
|
||||
add_shortcode( 'fedistream_playlist', array( $this, 'render_playlist' ) );
|
||||
add_shortcode( 'fedistream_latest_releases', array( $this, 'render_latest_releases' ) );
|
||||
add_shortcode( 'fedistream_popular_tracks', array( $this, 'render_popular_tracks' ) );
|
||||
add_shortcode( 'fedistream_artists', array( $this, 'render_artists_grid' ) );
|
||||
add_shortcode( 'fedistream_player', array( $this, 'render_player' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single artist shortcode.
|
||||
*
|
||||
* [fedistream_artist id="123" show_albums="true" show_tracks="true"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_artist( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'slug' => '',
|
||||
'show_albums' => 'true',
|
||||
'show_tracks' => 'true',
|
||||
'layout' => 'full', // full, card, compact
|
||||
),
|
||||
$atts,
|
||||
'fedistream_artist'
|
||||
);
|
||||
|
||||
$post = $this->get_post( $atts, 'fedistream_artist' );
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'post' => TemplateLoader::get_artist_data( $post ),
|
||||
'show_albums' => filter_var( $atts['show_albums'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'show_tracks' => filter_var( $atts['show_tracks'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'layout' => sanitize_key( $atts['layout'] ),
|
||||
);
|
||||
|
||||
$template = 'card' === $atts['layout'] ? 'partials/card-artist' : 'shortcodes/artist';
|
||||
|
||||
return $this->render_template( $template, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single album shortcode.
|
||||
*
|
||||
* [fedistream_album id="123" show_tracks="true"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_album( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'slug' => '',
|
||||
'show_tracks' => 'true',
|
||||
'layout' => 'full', // full, card, compact
|
||||
),
|
||||
$atts,
|
||||
'fedistream_album'
|
||||
);
|
||||
|
||||
$post = $this->get_post( $atts, 'fedistream_album' );
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'post' => TemplateLoader::get_album_data( $post ),
|
||||
'show_tracks' => filter_var( $atts['show_tracks'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'layout' => sanitize_key( $atts['layout'] ),
|
||||
);
|
||||
|
||||
$template = 'card' === $atts['layout'] ? 'partials/card-album' : 'shortcodes/album';
|
||||
|
||||
return $this->render_template( $template, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single track shortcode.
|
||||
*
|
||||
* [fedistream_track id="123" show_player="true"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_track( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'slug' => '',
|
||||
'show_player' => 'true',
|
||||
'layout' => 'full', // full, card, compact
|
||||
),
|
||||
$atts,
|
||||
'fedistream_track'
|
||||
);
|
||||
|
||||
$post = $this->get_post( $atts, 'fedistream_track' );
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'post' => TemplateLoader::get_track_data( $post ),
|
||||
'show_player' => filter_var( $atts['show_player'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'layout' => sanitize_key( $atts['layout'] ),
|
||||
);
|
||||
|
||||
$template = 'card' === $atts['layout'] ? 'partials/card-track' : 'shortcodes/track';
|
||||
|
||||
return $this->render_template( $template, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist shortcode.
|
||||
*
|
||||
* [fedistream_playlist id="123" show_tracks="true"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_playlist( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'id' => 0,
|
||||
'slug' => '',
|
||||
'show_tracks' => 'true',
|
||||
'layout' => 'full', // full, card, compact
|
||||
),
|
||||
$atts,
|
||||
'fedistream_playlist'
|
||||
);
|
||||
|
||||
$post = $this->get_post( $atts, 'fedistream_playlist' );
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'post' => TemplateLoader::get_playlist_data( $post ),
|
||||
'show_tracks' => filter_var( $atts['show_tracks'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'layout' => sanitize_key( $atts['layout'] ),
|
||||
);
|
||||
|
||||
$template = 'card' === $atts['layout'] ? 'partials/card-playlist' : 'shortcodes/playlist';
|
||||
|
||||
return $this->render_template( $template, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render latest releases shortcode.
|
||||
*
|
||||
* [fedistream_latest_releases count="6" type="album" columns="3"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_latest_releases( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'count' => 6,
|
||||
'type' => '', // album, ep, single, compilation or empty for all
|
||||
'columns' => 3,
|
||||
'artist' => 0, // Filter by artist ID
|
||||
),
|
||||
$atts,
|
||||
'fedistream_latest_releases'
|
||||
);
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => absint( $atts['count'] ),
|
||||
'orderby' => 'meta_value',
|
||||
'meta_key' => '_fedistream_release_date',
|
||||
'order' => 'DESC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
// Filter by album type.
|
||||
if ( ! empty( $atts['type'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_album_type',
|
||||
'value' => sanitize_key( $atts['type'] ),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by artist.
|
||||
if ( ! empty( $atts['artist'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_artist_id',
|
||||
'value' => absint( $atts['artist'] ),
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_album_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'posts' => $posts,
|
||||
'columns' => absint( $atts['columns'] ),
|
||||
'title' => __( 'Latest Releases', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
return $this->render_template( 'shortcodes/releases-grid', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render popular tracks shortcode.
|
||||
*
|
||||
* [fedistream_popular_tracks count="10" columns="1"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_popular_tracks( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'count' => 10,
|
||||
'columns' => 1,
|
||||
'artist' => 0, // Filter by artist ID
|
||||
'genre' => '', // Filter by genre slug
|
||||
),
|
||||
$atts,
|
||||
'fedistream_popular_tracks'
|
||||
);
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => absint( $atts['count'] ),
|
||||
'orderby' => 'meta_value_num',
|
||||
'meta_key' => '_fedistream_play_count',
|
||||
'order' => 'DESC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
// Filter by artist.
|
||||
if ( ! empty( $atts['artist'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_artist_ids',
|
||||
'value' => absint( $atts['artist'] ),
|
||||
'compare' => 'LIKE',
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by genre.
|
||||
if ( ! empty( $atts['genre'] ) ) {
|
||||
$query_args['tax_query'][] = array(
|
||||
'taxonomy' => 'fedistream_genre',
|
||||
'field' => 'slug',
|
||||
'terms' => sanitize_title( $atts['genre'] ),
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_track_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'posts' => $posts,
|
||||
'columns' => absint( $atts['columns'] ),
|
||||
'title' => __( 'Popular Tracks', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
return $this->render_template( 'shortcodes/tracks-list', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artists grid shortcode.
|
||||
*
|
||||
* [fedistream_artists count="12" columns="4" type="band"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_artists_grid( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'count' => 12,
|
||||
'columns' => 4,
|
||||
'type' => '', // solo, band, duo, collective, or empty for all
|
||||
'genre' => '', // Filter by genre slug
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
),
|
||||
$atts,
|
||||
'fedistream_artists'
|
||||
);
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => absint( $atts['count'] ),
|
||||
'orderby' => sanitize_key( $atts['orderby'] ),
|
||||
'order' => 'DESC' === strtoupper( $atts['order'] ) ? 'DESC' : 'ASC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
// Filter by artist type.
|
||||
if ( ! empty( $atts['type'] ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_artist_type',
|
||||
'value' => sanitize_key( $atts['type'] ),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by genre.
|
||||
if ( ! empty( $atts['genre'] ) ) {
|
||||
$query_args['tax_query'][] = array(
|
||||
'taxonomy' => 'fedistream_genre',
|
||||
'field' => 'slug',
|
||||
'terms' => sanitize_title( $atts['genre'] ),
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_artist_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'posts' => $posts,
|
||||
'columns' => absint( $atts['columns'] ),
|
||||
'title' => __( 'Artists', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
return $this->render_template( 'shortcodes/artists-grid', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render audio player shortcode.
|
||||
*
|
||||
* [fedistream_player track="123" autoplay="false"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_player( array $atts ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'track' => 0,
|
||||
'playlist' => 0,
|
||||
'album' => 0,
|
||||
'autoplay' => 'false',
|
||||
'style' => 'default', // default, compact, mini
|
||||
),
|
||||
$atts,
|
||||
'fedistream_player'
|
||||
);
|
||||
|
||||
$tracks = array();
|
||||
|
||||
// Get tracks based on source.
|
||||
if ( ! empty( $atts['track'] ) ) {
|
||||
$post = get_post( absint( $atts['track'] ) );
|
||||
if ( $post && 'fedistream_track' === $post->post_type ) {
|
||||
$tracks[] = TemplateLoader::get_track_data( $post );
|
||||
}
|
||||
} elseif ( ! empty( $atts['album'] ) ) {
|
||||
$album_id = absint( $atts['album'] );
|
||||
$track_ids = get_post_meta( $album_id, '_fedistream_track_ids', true );
|
||||
if ( is_array( $track_ids ) ) {
|
||||
foreach ( $track_ids as $track_id ) {
|
||||
$post = get_post( $track_id );
|
||||
if ( $post && 'fedistream_track' === $post->post_type ) {
|
||||
$tracks[] = TemplateLoader::get_track_data( $post );
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ( ! empty( $atts['playlist'] ) ) {
|
||||
$playlist_id = absint( $atts['playlist'] );
|
||||
$track_ids = get_post_meta( $playlist_id, '_fedistream_track_ids', true );
|
||||
if ( is_array( $track_ids ) ) {
|
||||
foreach ( $track_ids as $track_id ) {
|
||||
$post = get_post( $track_id );
|
||||
if ( $post && 'fedistream_track' === $post->post_type ) {
|
||||
$tracks[] = TemplateLoader::get_track_data( $post );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $tracks ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'tracks' => $tracks,
|
||||
'autoplay' => filter_var( $atts['autoplay'], FILTER_VALIDATE_BOOLEAN ),
|
||||
'style' => sanitize_key( $atts['style'] ),
|
||||
);
|
||||
|
||||
return $this->render_template( 'shortcodes/player', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post by ID or slug.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @param string $post_type Post type.
|
||||
* @return \WP_Post|null
|
||||
*/
|
||||
private function get_post( array $atts, string $post_type ): ?\WP_Post {
|
||||
if ( ! empty( $atts['id'] ) ) {
|
||||
$post = get_post( absint( $atts['id'] ) );
|
||||
if ( $post && $post->post_type === $post_type ) {
|
||||
return $post;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $atts['slug'] ) ) {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'name' => sanitize_title( $atts['slug'] ),
|
||||
'post_type' => $post_type,
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
if ( ! empty( $posts ) ) {
|
||||
return $posts[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Twig template.
|
||||
*
|
||||
* @param string $template Template name.
|
||||
* @param array $context Template context.
|
||||
* @return string
|
||||
*/
|
||||
private function render_template( string $template, array $context ): string {
|
||||
try {
|
||||
return $this->plugin->render( $template, $context );
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
return '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
528
includes/Frontend/TemplateLoader.php
Normal file
528
includes/Frontend/TemplateLoader.php
Normal file
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
/**
|
||||
* Template loader for frontend display.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend;
|
||||
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* TemplateLoader class.
|
||||
*
|
||||
* Handles loading custom templates for FediStream post types.
|
||||
*/
|
||||
class TemplateLoader {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_filter( 'template_include', array( $this, 'template_include' ) );
|
||||
add_filter( 'body_class', array( $this, 'body_class' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Include custom templates for FediStream post types.
|
||||
*
|
||||
* @param string $template Template path.
|
||||
* @return string Modified template path.
|
||||
*/
|
||||
public function template_include( string $template ): string {
|
||||
// Check if we're on a FediStream page.
|
||||
if ( is_singular( 'fedistream_artist' ) ) {
|
||||
return $this->get_template( 'single-artist' );
|
||||
}
|
||||
|
||||
if ( is_singular( 'fedistream_album' ) ) {
|
||||
return $this->get_template( 'single-album' );
|
||||
}
|
||||
|
||||
if ( is_singular( 'fedistream_track' ) ) {
|
||||
return $this->get_template( 'single-track' );
|
||||
}
|
||||
|
||||
if ( is_singular( 'fedistream_playlist' ) ) {
|
||||
return $this->get_template( 'single-playlist' );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive( 'fedistream_artist' ) ) {
|
||||
return $this->get_template( 'archive-artist' );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive( 'fedistream_album' ) ) {
|
||||
return $this->get_template( 'archive-album' );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive( 'fedistream_track' ) ) {
|
||||
return $this->get_template( 'archive-track' );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive( 'fedistream_playlist' ) ) {
|
||||
return $this->get_template( 'archive-playlist' );
|
||||
}
|
||||
|
||||
if ( is_tax( 'fedistream_genre' ) ) {
|
||||
return $this->get_template( 'taxonomy-genre' );
|
||||
}
|
||||
|
||||
if ( is_tax( 'fedistream_mood' ) ) {
|
||||
return $this->get_template( 'taxonomy-mood' );
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template file path.
|
||||
*
|
||||
* First checks theme for override, then uses plugin template.
|
||||
*
|
||||
* @param string $template_name Template name without extension.
|
||||
* @return string Template path.
|
||||
*/
|
||||
private function get_template( string $template_name ): string {
|
||||
// Check theme for override.
|
||||
$theme_template = locate_template(
|
||||
array(
|
||||
"fedistream/{$template_name}.php",
|
||||
"fedistream/{$template_name}.twig",
|
||||
)
|
||||
);
|
||||
|
||||
if ( $theme_template ) {
|
||||
return $theme_template;
|
||||
}
|
||||
|
||||
// Use plugin template wrapper.
|
||||
return WP_FEDISTREAM_PATH . 'includes/Frontend/template-wrapper.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add FediStream classes to body.
|
||||
*
|
||||
* @param array $classes Body classes.
|
||||
* @return array Modified classes.
|
||||
*/
|
||||
public function body_class( array $classes ): array {
|
||||
if ( $this->is_fedistream_page() ) {
|
||||
$classes[] = 'fedistream';
|
||||
|
||||
if ( is_singular( 'fedistream_artist' ) ) {
|
||||
$classes[] = 'fedistream-artist';
|
||||
$classes[] = 'fedistream-single';
|
||||
} elseif ( is_singular( 'fedistream_album' ) ) {
|
||||
$classes[] = 'fedistream-album';
|
||||
$classes[] = 'fedistream-single';
|
||||
} elseif ( is_singular( 'fedistream_track' ) ) {
|
||||
$classes[] = 'fedistream-track';
|
||||
$classes[] = 'fedistream-single';
|
||||
} elseif ( is_singular( 'fedistream_playlist' ) ) {
|
||||
$classes[] = 'fedistream-playlist';
|
||||
$classes[] = 'fedistream-single';
|
||||
} elseif ( is_post_type_archive( 'fedistream_artist' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-artists';
|
||||
} elseif ( is_post_type_archive( 'fedistream_album' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-albums';
|
||||
} elseif ( is_post_type_archive( 'fedistream_track' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-tracks';
|
||||
} elseif ( is_post_type_archive( 'fedistream_playlist' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-playlists';
|
||||
} elseif ( is_tax( 'fedistream_genre' ) || is_tax( 'fedistream_mood' ) ) {
|
||||
$classes[] = 'fedistream-archive';
|
||||
$classes[] = 'fedistream-taxonomy';
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current page is a FediStream page.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_fedistream_page(): bool {
|
||||
return is_singular( array( 'fedistream_artist', 'fedistream_album', 'fedistream_track', 'fedistream_playlist' ) )
|
||||
|| is_post_type_archive( array( 'fedistream_artist', 'fedistream_album', 'fedistream_track', 'fedistream_playlist' ) )
|
||||
|| is_tax( array( 'fedistream_genre', 'fedistream_mood', 'fedistream_license' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template context for current page.
|
||||
*
|
||||
* @return array Template context.
|
||||
*/
|
||||
public static function get_context(): array {
|
||||
$context = array(
|
||||
'site_name' => get_bloginfo( 'name' ),
|
||||
'site_url' => home_url(),
|
||||
'is_singular' => is_singular(),
|
||||
'is_archive' => is_archive(),
|
||||
'current_url' => get_permalink(),
|
||||
);
|
||||
|
||||
if ( is_singular() ) {
|
||||
global $post;
|
||||
$context['post'] = self::get_post_data( $post );
|
||||
}
|
||||
|
||||
if ( is_post_type_archive() || is_tax() ) {
|
||||
$context['posts'] = self::get_archive_posts();
|
||||
$context['pagination'] = self::get_pagination();
|
||||
$context['archive_title'] = self::get_archive_title();
|
||||
$context['archive_description'] = self::get_archive_description();
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get post data for template.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return array Post data.
|
||||
*/
|
||||
public static function get_post_data( \WP_Post $post ): array {
|
||||
$data = array(
|
||||
'id' => $post->ID,
|
||||
'title' => get_the_title( $post ),
|
||||
'content' => apply_filters( 'the_content', $post->post_content ),
|
||||
'excerpt' => get_the_excerpt( $post ),
|
||||
'permalink' => get_permalink( $post ),
|
||||
'thumbnail' => get_the_post_thumbnail_url( $post->ID, 'large' ),
|
||||
'date' => get_the_date( '', $post ),
|
||||
'author' => get_the_author_meta( 'display_name', $post->post_author ),
|
||||
);
|
||||
|
||||
// Add post type specific data.
|
||||
switch ( $post->post_type ) {
|
||||
case 'fedistream_artist':
|
||||
$data = array_merge( $data, self::get_artist_data( $post->ID ) );
|
||||
break;
|
||||
case 'fedistream_album':
|
||||
$data = array_merge( $data, self::get_album_data( $post->ID ) );
|
||||
break;
|
||||
case 'fedistream_track':
|
||||
$data = array_merge( $data, self::get_track_data( $post->ID ) );
|
||||
break;
|
||||
case 'fedistream_playlist':
|
||||
$data = array_merge( $data, self::get_playlist_data( $post->ID ) );
|
||||
break;
|
||||
}
|
||||
|
||||
// Add taxonomies.
|
||||
$data['genres'] = self::get_terms( $post->ID, 'fedistream_genre' );
|
||||
$data['moods'] = self::get_terms( $post->ID, 'fedistream_mood' );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist-specific data.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array Artist data.
|
||||
*/
|
||||
private static function get_artist_data( int $post_id ): array {
|
||||
$type = get_post_meta( $post_id, '_fedistream_artist_type', true ) ?: 'solo';
|
||||
$types = array(
|
||||
'solo' => __( 'Solo Artist', 'wp-fedistream' ),
|
||||
'band' => __( 'Band', 'wp-fedistream' ),
|
||||
'duo' => __( 'Duo', 'wp-fedistream' ),
|
||||
'collective' => __( 'Collective', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$albums = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_fedistream_album_artist',
|
||||
'meta_value' => $post_id,
|
||||
'orderby' => 'meta_value',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_fedistream_album_release_date',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
'order' => 'DESC',
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'artist_type' => $type,
|
||||
'artist_type_label' => $types[ $type ] ?? $types['solo'],
|
||||
'formed_date' => get_post_meta( $post_id, '_fedistream_artist_formed_date', true ),
|
||||
'location' => get_post_meta( $post_id, '_fedistream_artist_location', true ),
|
||||
'website' => get_post_meta( $post_id, '_fedistream_artist_website', true ),
|
||||
'social_links' => get_post_meta( $post_id, '_fedistream_artist_social_links', true ) ?: array(),
|
||||
'members' => get_post_meta( $post_id, '_fedistream_artist_members', true ) ?: array(),
|
||||
'albums' => array_map( array( __CLASS__, 'get_post_data' ), $albums ),
|
||||
'album_count' => count( $albums ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album-specific data.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array Album data.
|
||||
*/
|
||||
private static function get_album_data( int $post_id ): array {
|
||||
$type = get_post_meta( $post_id, '_fedistream_album_type', true ) ?: 'album';
|
||||
$types = array(
|
||||
'album' => __( 'Album', 'wp-fedistream' ),
|
||||
'ep' => __( 'EP', 'wp-fedistream' ),
|
||||
'single' => __( 'Single', 'wp-fedistream' ),
|
||||
'compilation' => __( 'Compilation', 'wp-fedistream' ),
|
||||
'live' => __( 'Live Album', 'wp-fedistream' ),
|
||||
'remix' => __( 'Remix Album', 'wp-fedistream' ),
|
||||
);
|
||||
$artist_id = get_post_meta( $post_id, '_fedistream_album_artist', true );
|
||||
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_fedistream_track_album',
|
||||
'meta_value' => $post_id,
|
||||
'orderby' => 'meta_value_num',
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_fedistream_track_number',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'album_type' => $type,
|
||||
'album_type_label' => $types[ $type ] ?? $types['album'],
|
||||
'release_date' => get_post_meta( $post_id, '_fedistream_album_release_date', true ),
|
||||
'release_year' => date( 'Y', strtotime( get_post_meta( $post_id, '_fedistream_album_release_date', true ) ?: 'now' ) ),
|
||||
'artist_id' => $artist_id,
|
||||
'artist_name' => $artist_id ? get_the_title( $artist_id ) : '',
|
||||
'artist_url' => $artist_id ? get_permalink( $artist_id ) : '',
|
||||
'upc' => get_post_meta( $post_id, '_fedistream_album_upc', true ),
|
||||
'catalog_number' => get_post_meta( $post_id, '_fedistream_album_catalog_number', true ),
|
||||
'total_tracks' => count( $tracks ),
|
||||
'total_duration' => (int) get_post_meta( $post_id, '_fedistream_album_total_duration', true ),
|
||||
'tracks' => array_map( array( __CLASS__, 'get_post_data' ), $tracks ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track-specific data.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array Track data.
|
||||
*/
|
||||
private static function get_track_data( int $post_id ): array {
|
||||
$album_id = get_post_meta( $post_id, '_fedistream_track_album', true );
|
||||
$audio_file = get_post_meta( $post_id, '_fedistream_track_audio_file', true );
|
||||
$artists = get_post_meta( $post_id, '_fedistream_track_artists', true ) ?: array();
|
||||
$duration = (int) get_post_meta( $post_id, '_fedistream_track_duration', true );
|
||||
|
||||
$artist_data = array();
|
||||
foreach ( $artists as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist ) {
|
||||
$artist_data[] = array(
|
||||
'id' => $artist_id,
|
||||
'name' => $artist->post_title,
|
||||
'url' => get_permalink( $artist_id ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'track_number' => (int) get_post_meta( $post_id, '_fedistream_track_number', true ),
|
||||
'disc_number' => (int) get_post_meta( $post_id, '_fedistream_track_disc_number', true ) ?: 1,
|
||||
'duration' => $duration,
|
||||
'duration_formatted' => $duration ? sprintf( '%d:%02d', floor( $duration / 60 ), $duration % 60 ) : '',
|
||||
'audio_url' => $audio_file ? wp_get_attachment_url( $audio_file ) : '',
|
||||
'audio_format' => get_post_meta( $post_id, '_fedistream_track_audio_format', true ),
|
||||
'bpm' => (int) get_post_meta( $post_id, '_fedistream_track_bpm', true ),
|
||||
'key' => get_post_meta( $post_id, '_fedistream_track_key', true ),
|
||||
'explicit' => (bool) get_post_meta( $post_id, '_fedistream_track_explicit', true ),
|
||||
'isrc' => get_post_meta( $post_id, '_fedistream_track_isrc', true ),
|
||||
'album_id' => $album_id,
|
||||
'album_title' => $album_id ? get_the_title( $album_id ) : '',
|
||||
'album_url' => $album_id ? get_permalink( $album_id ) : '',
|
||||
'album_artwork' => $album_id ? get_the_post_thumbnail_url( $album_id, 'medium' ) : '',
|
||||
'artists' => $artist_data,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist-specific data.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array Playlist data.
|
||||
*/
|
||||
private static function get_playlist_data( int $post_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
$duration = (int) get_post_meta( $post_id, '_fedistream_playlist_total_duration', true );
|
||||
|
||||
// Get tracks.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$track_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id FROM $table WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$post_id
|
||||
)
|
||||
);
|
||||
|
||||
$tracks = array();
|
||||
foreach ( $track_ids as $track_id ) {
|
||||
$track = get_post( $track_id );
|
||||
if ( $track && 'publish' === $track->post_status ) {
|
||||
$tracks[] = self::get_post_data( $track );
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'visibility' => get_post_meta( $post_id, '_fedistream_playlist_visibility', true ) ?: 'public',
|
||||
'collaborative' => (bool) get_post_meta( $post_id, '_fedistream_playlist_collaborative', true ),
|
||||
'federated' => (bool) get_post_meta( $post_id, '_fedistream_playlist_federated', true ),
|
||||
'track_count' => count( $tracks ),
|
||||
'total_duration' => $duration,
|
||||
'duration_formatted' => $duration >= 3600
|
||||
? sprintf( '%d:%02d:%02d', floor( $duration / 3600 ), floor( ( $duration % 3600 ) / 60 ), $duration % 60 )
|
||||
: sprintf( '%d:%02d', floor( $duration / 60 ), $duration % 60 ),
|
||||
'tracks' => $tracks,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get taxonomy terms for post.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $taxonomy Taxonomy name.
|
||||
* @return array Terms with name and URL.
|
||||
*/
|
||||
private static function get_terms( int $post_id, string $taxonomy ): array {
|
||||
$terms = get_the_terms( $post_id, $taxonomy );
|
||||
|
||||
if ( ! $terms || is_wp_error( $terms ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return array_map(
|
||||
function ( $term ) {
|
||||
return array(
|
||||
'id' => $term->term_id,
|
||||
'name' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
'url' => get_term_link( $term ),
|
||||
);
|
||||
},
|
||||
$terms
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get archive posts.
|
||||
*
|
||||
* @return array Posts for archive.
|
||||
*/
|
||||
private static function get_archive_posts(): array {
|
||||
global $wp_query;
|
||||
|
||||
$posts = array();
|
||||
if ( $wp_query->have_posts() ) {
|
||||
while ( $wp_query->have_posts() ) {
|
||||
$wp_query->the_post();
|
||||
$posts[] = self::get_post_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pagination data.
|
||||
*
|
||||
* @return array Pagination data.
|
||||
*/
|
||||
private static function get_pagination(): array {
|
||||
global $wp_query;
|
||||
|
||||
$total_pages = $wp_query->max_num_pages;
|
||||
$current = max( 1, get_query_var( 'paged' ) );
|
||||
|
||||
return array(
|
||||
'total_pages' => $total_pages,
|
||||
'current_page' => $current,
|
||||
'has_prev' => $current > 1,
|
||||
'has_next' => $current < $total_pages,
|
||||
'prev_url' => $current > 1 ? get_pagenum_link( $current - 1 ) : '',
|
||||
'next_url' => $current < $total_pages ? get_pagenum_link( $current + 1 ) : '',
|
||||
'links' => paginate_links(
|
||||
array(
|
||||
'total' => $total_pages,
|
||||
'current' => $current,
|
||||
'type' => 'array',
|
||||
'prev_text' => __( '« Previous', 'wp-fedistream' ),
|
||||
'next_text' => __( 'Next »', 'wp-fedistream' ),
|
||||
)
|
||||
) ?: array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get archive title.
|
||||
*
|
||||
* @return string Archive title.
|
||||
*/
|
||||
private static function get_archive_title(): string {
|
||||
if ( is_post_type_archive( 'fedistream_artist' ) ) {
|
||||
return __( 'Artists', 'wp-fedistream' );
|
||||
}
|
||||
if ( is_post_type_archive( 'fedistream_album' ) ) {
|
||||
return __( 'Albums', 'wp-fedistream' );
|
||||
}
|
||||
if ( is_post_type_archive( 'fedistream_track' ) ) {
|
||||
return __( 'Tracks', 'wp-fedistream' );
|
||||
}
|
||||
if ( is_post_type_archive( 'fedistream_playlist' ) ) {
|
||||
return __( 'Playlists', 'wp-fedistream' );
|
||||
}
|
||||
if ( is_tax() ) {
|
||||
return single_term_title( '', false );
|
||||
}
|
||||
return get_the_archive_title();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get archive description.
|
||||
*
|
||||
* @return string Archive description.
|
||||
*/
|
||||
private static function get_archive_description(): string {
|
||||
if ( is_tax() ) {
|
||||
return term_description();
|
||||
}
|
||||
return get_the_archive_description();
|
||||
}
|
||||
}
|
||||
38
includes/Frontend/Widgets.php
Normal file
38
includes/Frontend/Widgets.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* Widgets handler.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers and manages all plugin widgets.
|
||||
*/
|
||||
class Widgets {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'widgets_init', array( $this, 'register_widgets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all widgets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_widgets(): void {
|
||||
register_widget( Widgets\RecentReleasesWidget::class );
|
||||
register_widget( Widgets\PopularTracksWidget::class );
|
||||
register_widget( Widgets\FeaturedArtistWidget::class );
|
||||
register_widget( Widgets\NowPlayingWidget::class );
|
||||
}
|
||||
}
|
||||
162
includes/Frontend/Widgets/FeaturedArtistWidget.php
Normal file
162
includes/Frontend/Widgets/FeaturedArtistWidget.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Featured Artist Widget.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend\Widgets;
|
||||
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a featured artist.
|
||||
*/
|
||||
class FeaturedArtistWidget extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'fedistream_featured_artist',
|
||||
__( 'FediStream: Featured Artist', 'wp-fedistream' ),
|
||||
array(
|
||||
'description' => __( 'Display a featured artist.', 'wp-fedistream' ),
|
||||
'classname' => 'fedistream-widget fedistream-widget--featured-artist',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Featured Artist', 'wp-fedistream' );
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
$artist_id = ! empty( $instance['artist_id'] ) ? absint( $instance['artist_id'] ) : 0;
|
||||
$random = ! empty( $instance['random'] ) && $instance['random'];
|
||||
|
||||
$post = null;
|
||||
|
||||
if ( $random ) {
|
||||
// Get a random artist.
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => 1,
|
||||
'orderby' => 'rand',
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
if ( ! empty( $posts ) ) {
|
||||
$post = $posts[0];
|
||||
}
|
||||
} elseif ( $artist_id ) {
|
||||
$post = get_post( $artist_id );
|
||||
if ( $post && 'fedistream_artist' !== $post->post_type ) {
|
||||
$post = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_data = TemplateLoader::get_artist_data( $post );
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = Plugin::get_instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render(
|
||||
'widgets/featured-artist',
|
||||
array(
|
||||
'post' => $artist_data,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Featured Artist', 'wp-fedistream' );
|
||||
$artist_id = ! empty( $instance['artist_id'] ) ? absint( $instance['artist_id'] ) : 0;
|
||||
$random = ! empty( $instance['random'] ) && $instance['random'];
|
||||
|
||||
// Get all artists for dropdown.
|
||||
$artists = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'wp-fedistream' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'random' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'random' ) ); ?>" value="1" <?php checked( $random ); ?>>
|
||||
<?php esc_html_e( 'Show random artist', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'artist_id' ) ); ?>"><?php esc_html_e( 'Or select specific artist:', 'wp-fedistream' ); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'artist_id' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'artist_id' ) ); ?>">
|
||||
<option value=""><?php esc_html_e( '-- Select Artist --', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $artists as $artist ) : ?>
|
||||
<option value="<?php echo esc_attr( $artist->ID ); ?>" <?php selected( $artist_id, $artist->ID ); ?>>
|
||||
<?php echo esc_html( $artist->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['artist_id'] = ! empty( $new_instance['artist_id'] ) ? absint( $new_instance['artist_id'] ) : 0;
|
||||
$instance['random'] = ! empty( $new_instance['random'] );
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
111
includes/Frontend/Widgets/NowPlayingWidget.php
Normal file
111
includes/Frontend/Widgets/NowPlayingWidget.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
/**
|
||||
* Now Playing Widget.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend\Widgets;
|
||||
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the currently playing track (updates via JavaScript).
|
||||
*/
|
||||
class NowPlayingWidget extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'fedistream_now_playing',
|
||||
__( 'FediStream: Now Playing', 'wp-fedistream' ),
|
||||
array(
|
||||
'description' => __( 'Display the currently playing track.', 'wp-fedistream' ),
|
||||
'classname' => 'fedistream-widget fedistream-widget--now-playing',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Now Playing', 'wp-fedistream' );
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
$show_player = ! empty( $instance['show_player'] );
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = Plugin::get_instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render(
|
||||
'widgets/now-playing',
|
||||
array(
|
||||
'show_player' => $show_player,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Now Playing', 'wp-fedistream' );
|
||||
$show_player = ! empty( $instance['show_player'] );
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'wp-fedistream' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" id="<?php echo esc_attr( $this->get_field_id( 'show_player' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'show_player' ) ); ?>" value="1" <?php checked( $show_player ); ?>>
|
||||
<?php esc_html_e( 'Show player controls', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'This widget shows information about the currently playing track and updates automatically via JavaScript.', 'wp-fedistream' ); ?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['show_player'] = ! empty( $new_instance['show_player'] );
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
127
includes/Frontend/Widgets/PopularTracksWidget.php
Normal file
127
includes/Frontend/Widgets/PopularTracksWidget.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
/**
|
||||
* Popular Tracks Widget.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend\Widgets;
|
||||
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays popular tracks based on play count.
|
||||
*/
|
||||
class PopularTracksWidget extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'fedistream_popular_tracks',
|
||||
__( 'FediStream: Popular Tracks', 'wp-fedistream' ),
|
||||
array(
|
||||
'description' => __( 'Display popular tracks by play count.', 'wp-fedistream' ),
|
||||
'classname' => 'fedistream-widget fedistream-widget--popular-tracks',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Popular Tracks', 'wp-fedistream' );
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => $count,
|
||||
'orderby' => 'meta_value_num',
|
||||
'meta_key' => '_fedistream_play_count',
|
||||
'order' => 'DESC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_track_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = Plugin::get_instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render(
|
||||
'widgets/popular-tracks',
|
||||
array(
|
||||
'posts' => $posts,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Popular Tracks', 'wp-fedistream' );
|
||||
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'wp-fedistream' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"><?php esc_html_e( 'Number of tracks:', 'wp-fedistream' ); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>" type="number" min="1" max="20" value="<?php echo esc_attr( $count ); ?>">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['count'] = ! empty( $new_instance['count'] ) ? absint( $new_instance['count'] ) : 5;
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
147
includes/Frontend/Widgets/RecentReleasesWidget.php
Normal file
147
includes/Frontend/Widgets/RecentReleasesWidget.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* Recent Releases Widget.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Frontend\Widgets;
|
||||
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
use WP_FediStream\Plugin;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays recent album releases.
|
||||
*/
|
||||
class RecentReleasesWidget extends \WP_Widget {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'fedistream_recent_releases',
|
||||
__( 'FediStream: Recent Releases', 'wp-fedistream' ),
|
||||
array(
|
||||
'description' => __( 'Display recent album releases.', 'wp-fedistream' ),
|
||||
'classname' => 'fedistream-widget fedistream-widget--recent-releases',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function widget( $args, $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Recent Releases', 'wp-fedistream' );
|
||||
$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );
|
||||
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||
$type = ! empty( $instance['type'] ) ? $instance['type'] : '';
|
||||
|
||||
$query_args = array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => $count,
|
||||
'orderby' => 'meta_value',
|
||||
'meta_key' => '_fedistream_release_date',
|
||||
'order' => 'DESC',
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
if ( ! empty( $type ) ) {
|
||||
$query_args['meta_query'][] = array(
|
||||
'key' => '_fedistream_album_type',
|
||||
'value' => $type,
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $query_args );
|
||||
$posts = array();
|
||||
|
||||
if ( $query->have_posts() ) {
|
||||
while ( $query->have_posts() ) {
|
||||
$query->the_post();
|
||||
$posts[] = TemplateLoader::get_album_data( get_post() );
|
||||
}
|
||||
wp_reset_postdata();
|
||||
}
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
|
||||
if ( $title ) {
|
||||
echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
try {
|
||||
$plugin = Plugin::get_instance();
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render(
|
||||
'widgets/recent-releases',
|
||||
array(
|
||||
'posts' => $posts,
|
||||
)
|
||||
);
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<p class="fedistream-error">' . esc_html( $e->getMessage() ) . '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form.
|
||||
*
|
||||
* @param array $instance Previously saved values from database.
|
||||
* @return void
|
||||
*/
|
||||
public function form( $instance ): void {
|
||||
$title = ! empty( $instance['title'] ) ? $instance['title'] : __( 'Recent Releases', 'wp-fedistream' );
|
||||
$count = ! empty( $instance['count'] ) ? absint( $instance['count'] ) : 5;
|
||||
$type = ! empty( $instance['type'] ) ? $instance['type'] : '';
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"><?php esc_html_e( 'Title:', 'wp-fedistream' ); ?></label>
|
||||
<input class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>" type="text" value="<?php echo esc_attr( $title ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>"><?php esc_html_e( 'Number of releases:', 'wp-fedistream' ); ?></label>
|
||||
<input class="tiny-text" id="<?php echo esc_attr( $this->get_field_id( 'count' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'count' ) ); ?>" type="number" min="1" max="20" value="<?php echo esc_attr( $count ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="<?php echo esc_attr( $this->get_field_id( 'type' ) ); ?>"><?php esc_html_e( 'Release type:', 'wp-fedistream' ); ?></label>
|
||||
<select class="widefat" id="<?php echo esc_attr( $this->get_field_id( 'type' ) ); ?>" name="<?php echo esc_attr( $this->get_field_name( 'type' ) ); ?>">
|
||||
<option value="" <?php selected( $type, '' ); ?>><?php esc_html_e( 'All types', 'wp-fedistream' ); ?></option>
|
||||
<option value="album" <?php selected( $type, 'album' ); ?>><?php esc_html_e( 'Album', 'wp-fedistream' ); ?></option>
|
||||
<option value="ep" <?php selected( $type, 'ep' ); ?>><?php esc_html_e( 'EP', 'wp-fedistream' ); ?></option>
|
||||
<option value="single" <?php selected( $type, 'single' ); ?>><?php esc_html_e( 'Single', 'wp-fedistream' ); ?></option>
|
||||
<option value="compilation" <?php selected( $type, 'compilation' ); ?>><?php esc_html_e( 'Compilation', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved.
|
||||
*
|
||||
* @param array $new_instance Values just sent to be saved.
|
||||
* @param array $old_instance Previously saved values from database.
|
||||
* @return array Updated safe values to be saved.
|
||||
*/
|
||||
public function update( $new_instance, $old_instance ): array {
|
||||
$instance = array();
|
||||
$instance['title'] = ! empty( $new_instance['title'] ) ? sanitize_text_field( $new_instance['title'] ) : '';
|
||||
$instance['count'] = ! empty( $new_instance['count'] ) ? absint( $new_instance['count'] ) : 5;
|
||||
$instance['type'] = ! empty( $new_instance['type'] ) ? sanitize_key( $new_instance['type'] ) : '';
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
78
includes/Frontend/template-wrapper.php
Normal file
78
includes/Frontend/template-wrapper.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* Template wrapper for FediStream Twig templates.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use WP_FediStream\Plugin;
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
|
||||
// Get template context.
|
||||
$context = TemplateLoader::get_context();
|
||||
|
||||
// Determine template name.
|
||||
$template_name = '';
|
||||
|
||||
if ( is_singular( 'fedistream_artist' ) ) {
|
||||
$template_name = 'single/artist';
|
||||
} elseif ( is_singular( 'fedistream_album' ) ) {
|
||||
$template_name = 'single/album';
|
||||
} elseif ( is_singular( 'fedistream_track' ) ) {
|
||||
$template_name = 'single/track';
|
||||
} elseif ( is_singular( 'fedistream_playlist' ) ) {
|
||||
$template_name = 'single/playlist';
|
||||
} elseif ( is_post_type_archive( 'fedistream_artist' ) ) {
|
||||
$template_name = 'archive/artist';
|
||||
} elseif ( is_post_type_archive( 'fedistream_album' ) ) {
|
||||
$template_name = 'archive/album';
|
||||
} elseif ( is_post_type_archive( 'fedistream_track' ) ) {
|
||||
$template_name = 'archive/track';
|
||||
} elseif ( is_post_type_archive( 'fedistream_playlist' ) ) {
|
||||
$template_name = 'archive/playlist';
|
||||
} elseif ( is_tax( 'fedistream_genre' ) ) {
|
||||
$template_name = 'archive/taxonomy';
|
||||
$context['taxonomy_name'] = __( 'Genre', 'wp-fedistream' );
|
||||
} elseif ( is_tax( 'fedistream_mood' ) ) {
|
||||
$template_name = 'archive/taxonomy';
|
||||
$context['taxonomy_name'] = __( 'Mood', 'wp-fedistream' );
|
||||
}
|
||||
|
||||
// Get the plugin instance.
|
||||
$plugin = Plugin::get_instance();
|
||||
|
||||
get_header();
|
||||
?>
|
||||
|
||||
<main id="fedistream-content" class="fedistream-main">
|
||||
<?php
|
||||
if ( $template_name ) {
|
||||
try {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $plugin->render( $template_name, $context );
|
||||
} catch ( \Exception $e ) {
|
||||
if ( WP_DEBUG ) {
|
||||
echo '<div class="fedistream-error">';
|
||||
echo '<p>' . esc_html__( 'Template Error:', 'wp-fedistream' ) . ' ' . esc_html( $e->getMessage() ) . '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to default content.
|
||||
if ( have_posts() ) {
|
||||
while ( have_posts() ) {
|
||||
the_post();
|
||||
the_content();
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</main>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
462
includes/Installer.php
Normal file
462
includes/Installer.php
Normal file
@@ -0,0 +1,462 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin installer class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream;
|
||||
|
||||
use WP_FediStream\Roles\Capabilities;
|
||||
use WP_FediStream\Taxonomies\Genre;
|
||||
use WP_FediStream\Taxonomies\Mood;
|
||||
use WP_FediStream\Taxonomies\License;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles plugin activation, deactivation, and uninstallation.
|
||||
*/
|
||||
class Installer {
|
||||
|
||||
/**
|
||||
* Plugin activation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate(): void {
|
||||
self::create_tables();
|
||||
self::create_directories();
|
||||
self::set_default_options();
|
||||
self::schedule_events();
|
||||
|
||||
// Install roles and capabilities.
|
||||
Capabilities::install();
|
||||
|
||||
// Store installed version.
|
||||
update_option( 'wp_fedistream_version', WP_FEDISTREAM_VERSION );
|
||||
|
||||
// Flush rewrite rules for custom post types.
|
||||
flush_rewrite_rules();
|
||||
|
||||
// Install default taxonomy terms (after rewrite rules flush).
|
||||
// Schedule for next page load since taxonomies need to be registered first.
|
||||
update_option( 'wp_fedistream_install_defaults', 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deactivate(): void {
|
||||
self::unschedule_events();
|
||||
|
||||
// Flush rewrite rules.
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin uninstallation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall(): void {
|
||||
// Only run if uninstall is explicitly requested.
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::delete_tables();
|
||||
self::delete_options();
|
||||
self::delete_user_meta();
|
||||
self::delete_transients();
|
||||
self::delete_posts();
|
||||
self::delete_terms();
|
||||
|
||||
// Remove roles and capabilities.
|
||||
Capabilities::uninstall();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all plugin posts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_posts(): void {
|
||||
$post_types = array(
|
||||
'fedistream_artist',
|
||||
'fedistream_album',
|
||||
'fedistream_track',
|
||||
'fedistream_playlist',
|
||||
);
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => $post_type,
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'any',
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $posts as $post_id ) {
|
||||
wp_delete_post( $post_id, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all plugin taxonomy terms.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_terms(): void {
|
||||
$taxonomies = array(
|
||||
'fedistream_genre',
|
||||
'fedistream_mood',
|
||||
'fedistream_license',
|
||||
);
|
||||
|
||||
foreach ( $taxonomies as $taxonomy ) {
|
||||
$terms = get_terms(
|
||||
array(
|
||||
'taxonomy' => $taxonomy,
|
||||
'hide_empty' => false,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! is_wp_error( $terms ) ) {
|
||||
foreach ( $terms as $term_id ) {
|
||||
wp_delete_term( $term_id, $taxonomy );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install default taxonomy terms.
|
||||
*
|
||||
* Called on first load after activation when taxonomies are registered.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_defaults(): void {
|
||||
if ( ! get_option( 'wp_fedistream_install_defaults' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Install default genres.
|
||||
Genre::install_defaults();
|
||||
|
||||
// Install default moods.
|
||||
Mood::install_defaults();
|
||||
|
||||
// Install default licenses.
|
||||
License::install_defaults();
|
||||
|
||||
// Clear the flag.
|
||||
delete_option( 'wp_fedistream_install_defaults' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom database tables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function create_tables(): void {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Track plays table.
|
||||
$table_plays = $wpdb->prefix . 'fedistream_plays';
|
||||
$sql_plays = "CREATE TABLE $table_plays (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
track_id bigint(20) unsigned NOT NULL,
|
||||
user_id bigint(20) unsigned DEFAULT NULL,
|
||||
remote_actor varchar(255) DEFAULT NULL,
|
||||
played_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
play_duration int(11) unsigned DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY track_id (track_id),
|
||||
KEY user_id (user_id),
|
||||
KEY played_at (played_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// Playlist tracks table (many-to-many relationship).
|
||||
$table_playlist_tracks = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
$sql_playlist_tracks = "CREATE TABLE $table_playlist_tracks (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
playlist_id bigint(20) unsigned NOT NULL,
|
||||
track_id bigint(20) unsigned NOT NULL,
|
||||
position int(11) unsigned NOT NULL DEFAULT 0,
|
||||
added_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY playlist_track (playlist_id, track_id),
|
||||
KEY playlist_id (playlist_id),
|
||||
KEY track_id (track_id),
|
||||
KEY position (position)
|
||||
) $charset_collate;";
|
||||
|
||||
// ActivityPub followers table.
|
||||
$table_followers = $wpdb->prefix . 'fedistream_followers';
|
||||
$sql_followers = "CREATE TABLE $table_followers (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
artist_id bigint(20) unsigned NOT NULL,
|
||||
follower_uri varchar(2083) NOT NULL,
|
||||
follower_name varchar(255) DEFAULT NULL,
|
||||
follower_icon varchar(2083) DEFAULT NULL,
|
||||
inbox varchar(2083) DEFAULT NULL,
|
||||
shared_inbox varchar(2083) DEFAULT NULL,
|
||||
activity_data longtext DEFAULT NULL,
|
||||
followed_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY artist_follower (artist_id, follower_uri(191)),
|
||||
KEY artist_id (artist_id),
|
||||
KEY followed_at (followed_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// WooCommerce purchases table.
|
||||
$table_purchases = $wpdb->prefix . 'fedistream_purchases';
|
||||
$sql_purchases = "CREATE TABLE $table_purchases (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
content_type varchar(50) NOT NULL,
|
||||
content_id bigint(20) unsigned NOT NULL,
|
||||
order_id bigint(20) unsigned NOT NULL,
|
||||
purchased_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_content (user_id, content_type, content_id),
|
||||
KEY user_id (user_id),
|
||||
KEY content_type (content_type),
|
||||
KEY content_id (content_id),
|
||||
KEY order_id (order_id)
|
||||
) $charset_collate;";
|
||||
|
||||
// User favorites table.
|
||||
$table_favorites = $wpdb->prefix . 'fedistream_favorites';
|
||||
$sql_favorites = "CREATE TABLE $table_favorites (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
content_type varchar(50) NOT NULL,
|
||||
content_id bigint(20) unsigned NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_content (user_id, content_type, content_id),
|
||||
KEY user_id (user_id),
|
||||
KEY content_type (content_type),
|
||||
KEY content_id (content_id),
|
||||
KEY created_at (created_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// User follows table (local follows, not ActivityPub).
|
||||
$table_user_follows = $wpdb->prefix . 'fedistream_user_follows';
|
||||
$sql_user_follows = "CREATE TABLE $table_user_follows (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
artist_id bigint(20) unsigned NOT NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY user_artist (user_id, artist_id),
|
||||
KEY user_id (user_id),
|
||||
KEY artist_id (artist_id),
|
||||
KEY created_at (created_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// Listening history table.
|
||||
$table_history = $wpdb->prefix . 'fedistream_listening_history';
|
||||
$sql_history = "CREATE TABLE $table_history (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
track_id bigint(20) unsigned NOT NULL,
|
||||
played_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY track_id (track_id),
|
||||
KEY played_at (played_at),
|
||||
KEY user_played (user_id, played_at)
|
||||
) $charset_collate;";
|
||||
|
||||
// Notifications table.
|
||||
$table_notifications = $wpdb->prefix . 'fedistream_notifications';
|
||||
$sql_notifications = "CREATE TABLE $table_notifications (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
user_id bigint(20) unsigned NOT NULL,
|
||||
type varchar(50) NOT NULL,
|
||||
title varchar(255) NOT NULL,
|
||||
message text NOT NULL,
|
||||
data longtext DEFAULT NULL,
|
||||
is_read tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
read_at datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY user_id (user_id),
|
||||
KEY type (type),
|
||||
KEY is_read (is_read),
|
||||
KEY created_at (created_at),
|
||||
KEY user_unread (user_id, is_read)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
dbDelta( $sql_plays );
|
||||
dbDelta( $sql_playlist_tracks );
|
||||
dbDelta( $sql_followers );
|
||||
dbDelta( $sql_purchases );
|
||||
dbDelta( $sql_favorites );
|
||||
dbDelta( $sql_user_follows );
|
||||
dbDelta( $sql_history );
|
||||
dbDelta( $sql_notifications );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create required directories.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function create_directories(): void {
|
||||
$directories = array(
|
||||
WP_FEDISTREAM_PATH . 'cache/twig',
|
||||
);
|
||||
|
||||
foreach ( $directories as $directory ) {
|
||||
if ( ! file_exists( $directory ) ) {
|
||||
wp_mkdir_p( $directory );
|
||||
}
|
||||
}
|
||||
|
||||
// Create .htaccess to protect cache directory.
|
||||
$htaccess = WP_FEDISTREAM_PATH . 'cache/.htaccess';
|
||||
if ( ! file_exists( $htaccess ) ) {
|
||||
file_put_contents( $htaccess, 'Deny from all' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default plugin options.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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',
|
||||
);
|
||||
|
||||
foreach ( $defaults as $option => $value ) {
|
||||
if ( false === get_option( $option ) ) {
|
||||
add_option( $option, $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule cron events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function schedule_events(): void {
|
||||
if ( ! wp_next_scheduled( 'wp_fedistream_daily_cleanup' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'wp_fedistream_daily_cleanup' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule cron events.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function unschedule_events(): void {
|
||||
$timestamp = wp_next_scheduled( 'wp_fedistream_daily_cleanup' );
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, 'wp_fedistream_daily_cleanup' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete custom database tables.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_tables(): void {
|
||||
global $wpdb;
|
||||
|
||||
$tables = array(
|
||||
$wpdb->prefix . 'fedistream_plays',
|
||||
$wpdb->prefix . 'fedistream_playlist_tracks',
|
||||
$wpdb->prefix . 'fedistream_followers',
|
||||
$wpdb->prefix . 'fedistream_purchases',
|
||||
$wpdb->prefix . 'fedistream_reactions',
|
||||
$wpdb->prefix . 'fedistream_favorites',
|
||||
$wpdb->prefix . 'fedistream_user_follows',
|
||||
$wpdb->prefix . 'fedistream_listening_history',
|
||||
$wpdb->prefix . 'fedistream_notifications',
|
||||
);
|
||||
|
||||
foreach ( $tables as $table ) {
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$wpdb->query( "DROP TABLE IF EXISTS $table" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete plugin options.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_options(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Delete all options starting with wp_fedistream_.
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $wpdb->options WHERE option_name LIKE %s",
|
||||
'wp_fedistream_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user meta.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_user_meta(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Delete all user meta starting with wp_fedistream_.
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $wpdb->usermeta WHERE meta_key LIKE %s",
|
||||
'wp_fedistream_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete transients.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function delete_transients(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Delete all transients starting with wp_fedistream_.
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM $wpdb->options WHERE option_name LIKE %s OR option_name LIKE %s",
|
||||
'_transient_wp_fedistream_%',
|
||||
'_transient_timeout_wp_fedistream_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
603
includes/Plugin.php
Normal file
603
includes/Plugin.php
Normal file
@@ -0,0 +1,603 @@
|
||||
<?php
|
||||
/**
|
||||
* Main plugin class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream;
|
||||
|
||||
use WP_FediStream\ActivityPub\Integration as ActivityPubIntegration;
|
||||
use WP_FediStream\ActivityPub\RestApi as ActivityPubRestApi;
|
||||
use WP_FediStream\Admin\ListColumns;
|
||||
use WP_FediStream\Frontend\Ajax;
|
||||
use WP_FediStream\Frontend\Shortcodes;
|
||||
use WP_FediStream\Frontend\TemplateLoader;
|
||||
use WP_FediStream\Frontend\Widgets;
|
||||
use WP_FediStream\PostTypes\Artist;
|
||||
use WP_FediStream\WooCommerce\Integration as WooCommerceIntegration;
|
||||
use WP_FediStream\WooCommerce\DigitalDelivery;
|
||||
use WP_FediStream\WooCommerce\StreamingAccess;
|
||||
use WP_FediStream\PostTypes\Album;
|
||||
use WP_FediStream\PostTypes\Track;
|
||||
use WP_FediStream\PostTypes\Playlist;
|
||||
use WP_FediStream\Taxonomies\Genre;
|
||||
use WP_FediStream\Taxonomies\Mood;
|
||||
use WP_FediStream\Taxonomies\License;
|
||||
use WP_FediStream\User\Library as UserLibrary;
|
||||
use WP_FediStream\User\LibraryPage;
|
||||
use WP_FediStream\User\Notifications;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin singleton class.
|
||||
*
|
||||
* Initializes and manages all plugin components.
|
||||
*/
|
||||
final class Plugin {
|
||||
|
||||
/**
|
||||
* Singleton instance.
|
||||
*
|
||||
* @var Plugin|null
|
||||
*/
|
||||
private static ?Plugin $instance = null;
|
||||
|
||||
/**
|
||||
* Twig environment instance.
|
||||
*
|
||||
* @var \Twig\Environment|null
|
||||
*/
|
||||
private ?\Twig\Environment $twig = null;
|
||||
|
||||
/**
|
||||
* Post type instances.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $post_types = array();
|
||||
|
||||
/**
|
||||
* Taxonomy instances.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $taxonomies = array();
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
*
|
||||
* @return Plugin
|
||||
*/
|
||||
public static function get_instance(): Plugin {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Private constructor to enforce singleton pattern.
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_twig();
|
||||
$this->init_components();
|
||||
$this->init_hooks();
|
||||
$this->load_textdomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent cloning.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function __clone() {}
|
||||
|
||||
/**
|
||||
* Prevent unserialization.
|
||||
*
|
||||
* @throws \Exception Always throws to prevent unserialization.
|
||||
* @return void
|
||||
*/
|
||||
public function __wakeup(): void {
|
||||
throw new \Exception( 'Cannot unserialize singleton' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Twig template engine.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_twig(): void {
|
||||
$loader = new \Twig\Loader\FilesystemLoader( WP_FEDISTREAM_PATH . 'templates' );
|
||||
|
||||
$this->twig = new \Twig\Environment(
|
||||
$loader,
|
||||
array(
|
||||
'cache' => WP_FEDISTREAM_PATH . 'cache/twig',
|
||||
'auto_reload' => WP_DEBUG,
|
||||
)
|
||||
);
|
||||
|
||||
// Add WordPress escaping functions.
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_html', 'esc_html' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_attr', 'esc_attr' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_url', 'esc_url' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'esc_js', 'esc_js' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( 'wp_nonce_field', 'wp_nonce_field', array( 'is_safe' => array( 'html' ) ) ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( '__', '__' ) );
|
||||
$this->twig->addFunction( new \Twig\TwigFunction( '_e', '_e' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize plugin components.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_components(): void {
|
||||
// Initialize post types.
|
||||
$this->post_types['artist'] = new Artist();
|
||||
$this->post_types['album'] = new Album();
|
||||
$this->post_types['track'] = new Track();
|
||||
$this->post_types['playlist'] = new Playlist();
|
||||
|
||||
// Initialize taxonomies.
|
||||
$this->taxonomies['genre'] = new Genre();
|
||||
$this->taxonomies['mood'] = new Mood();
|
||||
$this->taxonomies['license'] = new License();
|
||||
|
||||
// Initialize admin components.
|
||||
if ( is_admin() ) {
|
||||
new ListColumns();
|
||||
}
|
||||
|
||||
// Initialize frontend components.
|
||||
if ( ! is_admin() ) {
|
||||
new TemplateLoader();
|
||||
new Shortcodes();
|
||||
}
|
||||
|
||||
// Initialize widgets (always needed for admin widget management).
|
||||
new Widgets();
|
||||
|
||||
// Initialize AJAX handlers.
|
||||
new Ajax();
|
||||
|
||||
// Initialize ActivityPub integration.
|
||||
if ( get_option( 'wp_fedistream_enable_activitypub', 1 ) ) {
|
||||
new ActivityPubIntegration();
|
||||
new ActivityPubRestApi();
|
||||
}
|
||||
|
||||
// Initialize WooCommerce integration.
|
||||
if ( get_option( 'wp_fedistream_enable_woocommerce', 0 ) && $this->is_woocommerce_active() ) {
|
||||
new WooCommerceIntegration();
|
||||
new DigitalDelivery();
|
||||
new StreamingAccess();
|
||||
}
|
||||
|
||||
// Initialize user library and notifications.
|
||||
new UserLibrary();
|
||||
new LibraryPage();
|
||||
new Notifications();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_action( 'init', array( $this, 'maybe_install_defaults' ), 20 );
|
||||
add_action( 'admin_menu', array( $this, 'add_admin_menu' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe install default taxonomy terms.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_install_defaults(): void {
|
||||
Installer::install_defaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin textdomain.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function load_textdomain(): void {
|
||||
load_plugin_textdomain(
|
||||
'wp-fedistream',
|
||||
false,
|
||||
dirname( WP_FEDISTREAM_BASENAME ) . '/languages'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_admin_menu(): void {
|
||||
// Main menu.
|
||||
add_menu_page(
|
||||
__( 'FediStream', 'wp-fedistream' ),
|
||||
__( 'FediStream', 'wp-fedistream' ),
|
||||
'edit_fedistream_tracks',
|
||||
'fedistream',
|
||||
array( $this, 'render_dashboard_page' ),
|
||||
'dashicons-format-audio',
|
||||
30
|
||||
);
|
||||
|
||||
// Dashboard submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Dashboard', 'wp-fedistream' ),
|
||||
__( 'Dashboard', 'wp-fedistream' ),
|
||||
'edit_fedistream_tracks',
|
||||
'fedistream',
|
||||
array( $this, 'render_dashboard_page' )
|
||||
);
|
||||
|
||||
// Artists submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Artists', 'wp-fedistream' ),
|
||||
__( 'Artists', 'wp-fedistream' ),
|
||||
'edit_fedistream_artists',
|
||||
'edit.php?post_type=fedistream_artist'
|
||||
);
|
||||
|
||||
// Albums submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Albums', 'wp-fedistream' ),
|
||||
__( 'Albums', 'wp-fedistream' ),
|
||||
'edit_fedistream_albums',
|
||||
'edit.php?post_type=fedistream_album'
|
||||
);
|
||||
|
||||
// Tracks submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Tracks', 'wp-fedistream' ),
|
||||
__( 'Tracks', 'wp-fedistream' ),
|
||||
'edit_fedistream_tracks',
|
||||
'edit.php?post_type=fedistream_track'
|
||||
);
|
||||
|
||||
// Playlists submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Playlists', 'wp-fedistream' ),
|
||||
__( 'Playlists', 'wp-fedistream' ),
|
||||
'edit_fedistream_playlists',
|
||||
'edit.php?post_type=fedistream_playlist'
|
||||
);
|
||||
|
||||
// Genres submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Genres', 'wp-fedistream' ),
|
||||
__( 'Genres', 'wp-fedistream' ),
|
||||
'manage_fedistream_genres',
|
||||
'edit-tags.php?taxonomy=fedistream_genre'
|
||||
);
|
||||
|
||||
// Settings submenu.
|
||||
add_submenu_page(
|
||||
'fedistream',
|
||||
__( 'Settings', 'wp-fedistream' ),
|
||||
__( 'Settings', 'wp-fedistream' ),
|
||||
'manage_fedistream_settings',
|
||||
'fedistream-settings',
|
||||
array( $this, 'render_settings_page' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render dashboard page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_dashboard_page(): void {
|
||||
// Get stats.
|
||||
$artist_count = wp_count_posts( 'fedistream_artist' )->publish ?? 0;
|
||||
$album_count = wp_count_posts( 'fedistream_album' )->publish ?? 0;
|
||||
$track_count = wp_count_posts( 'fedistream_track' )->publish ?? 0;
|
||||
$playlist_count = wp_count_posts( 'fedistream_playlist' )->publish ?? 0;
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'FediStream Dashboard', 'wp-fedistream' ); ?></h1>
|
||||
|
||||
<div class="fedistream-dashboard">
|
||||
<div class="fedistream-stats" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin: 20px 0;">
|
||||
<div class="fedistream-stat-box" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Artists', 'wp-fedistream' ); ?></h3>
|
||||
<p style="font-size: 2em; margin: 0; color: #2271b1;"><?php echo esc_html( $artist_count ); ?></p>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=fedistream_artist' ) ); ?>"><?php esc_html_e( 'Manage Artists', 'wp-fedistream' ); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-stat-box" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Albums', 'wp-fedistream' ); ?></h3>
|
||||
<p style="font-size: 2em; margin: 0; color: #2271b1;"><?php echo esc_html( $album_count ); ?></p>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=fedistream_album' ) ); ?>"><?php esc_html_e( 'Manage Albums', 'wp-fedistream' ); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-stat-box" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Tracks', 'wp-fedistream' ); ?></h3>
|
||||
<p style="font-size: 2em; margin: 0; color: #2271b1;"><?php echo esc_html( $track_count ); ?></p>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=fedistream_track' ) ); ?>"><?php esc_html_e( 'Manage Tracks', 'wp-fedistream' ); ?></a>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-stat-box" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h3 style="margin: 0 0 10px;"><?php esc_html_e( 'Playlists', 'wp-fedistream' ); ?></h3>
|
||||
<p style="font-size: 2em; margin: 0; color: #2271b1;"><?php echo esc_html( $playlist_count ); ?></p>
|
||||
<a href="<?php echo esc_url( admin_url( 'edit.php?post_type=fedistream_playlist' ) ); ?>"><?php esc_html_e( 'Manage Playlists', 'wp-fedistream' ); ?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-quick-actions" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px; margin: 20px 0;">
|
||||
<h2><?php esc_html_e( 'Quick Actions', 'wp-fedistream' ); ?></h2>
|
||||
<p>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=fedistream_artist' ) ); ?>" class="button button-primary"><?php esc_html_e( 'Add Artist', 'wp-fedistream' ); ?></a>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=fedistream_album' ) ); ?>" class="button"><?php esc_html_e( 'Add Album', 'wp-fedistream' ); ?></a>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=fedistream_track' ) ); ?>" class="button"><?php esc_html_e( 'Add Track', 'wp-fedistream' ); ?></a>
|
||||
<a href="<?php echo esc_url( admin_url( 'post-new.php?post_type=fedistream_playlist' ) ); ?>" class="button"><?php esc_html_e( 'Add Playlist', 'wp-fedistream' ); ?></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-info" style="background: #fff; padding: 20px; border: 1px solid #ccd0d4; border-radius: 4px;">
|
||||
<h2><?php esc_html_e( 'Getting Started', 'wp-fedistream' ); ?></h2>
|
||||
<ol>
|
||||
<li><?php esc_html_e( 'Add your artists or bands.', 'wp-fedistream' ); ?></li>
|
||||
<li><?php esc_html_e( 'Create albums and assign them to artists.', 'wp-fedistream' ); ?></li>
|
||||
<li><?php esc_html_e( 'Upload tracks and add them to albums.', 'wp-fedistream' ); ?></li>
|
||||
<li><?php esc_html_e( 'Create playlists to curate your music.', 'wp-fedistream' ); ?></li>
|
||||
<li><?php esc_html_e( 'Share your music via ActivityPub to the Fediverse!', 'wp-fedistream' ); ?></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render settings page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function render_settings_page(): void {
|
||||
// Check user capabilities.
|
||||
if ( ! current_user_can( 'manage_fedistream_settings' ) ) {
|
||||
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' ) ) );
|
||||
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Settings saved.', 'wp-fedistream' ) . '</p></div>';
|
||||
}
|
||||
|
||||
// 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' );
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e( 'FediStream Settings', 'wp-fedistream' ); ?></h1>
|
||||
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'fedistream_save_settings', 'fedistream_settings_nonce' ); ?>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'ActivityPub Integration', 'wp-fedistream' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_activitypub" value="1" <?php checked( $enable_activitypub, 1 ); ?>>
|
||||
<?php esc_html_e( 'Enable ActivityPub features', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e( 'Publish releases to the Fediverse and allow followers.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'WooCommerce Integration', 'wp-fedistream' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="enable_woocommerce" value="1" <?php checked( $enable_woocommerce, 1 ); ?> <?php disabled( ! $this->is_woocommerce_active() ); ?>>
|
||||
<?php esc_html_e( 'Enable WooCommerce features', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<?php if ( ! $this->is_woocommerce_active() ) : ?>
|
||||
<p class="description" style="color: #d63638;"><?php esc_html_e( 'WooCommerce is not installed or active.', 'wp-fedistream' ); ?></p>
|
||||
<?php else : ?>
|
||||
<p class="description"><?php esc_html_e( 'Sell albums and tracks through WooCommerce.', 'wp-fedistream' ); ?></p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="max_upload_size"><?php esc_html_e( 'Max Upload Size', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="max_upload_size" id="max_upload_size" value="<?php echo esc_attr( $max_upload_size ); ?>" min="1" max="500" class="small-text"> MB
|
||||
<p class="description"><?php esc_html_e( 'Maximum file size for audio uploads.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="default_license"><?php esc_html_e( 'Default License', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="default_license" id="default_license">
|
||||
<option value="all-rights-reserved" <?php selected( $default_license, 'all-rights-reserved' ); ?>><?php esc_html_e( 'All Rights Reserved', 'wp-fedistream' ); ?></option>
|
||||
<option value="cc-by" <?php selected( $default_license, 'cc-by' ); ?>>CC BY</option>
|
||||
<option value="cc-by-sa" <?php selected( $default_license, 'cc-by-sa' ); ?>>CC BY-SA</option>
|
||||
<option value="cc-by-nc" <?php selected( $default_license, 'cc-by-nc' ); ?>>CC BY-NC</option>
|
||||
<option value="cc-by-nc-sa" <?php selected( $default_license, 'cc-by-nc-sa' ); ?>>CC BY-NC-SA</option>
|
||||
<option value="cc0" <?php selected( $default_license, 'cc0' ); ?>>CC0 (Public Domain)</option>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'Default license for new uploads.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button(); ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin assets.
|
||||
*
|
||||
* @param string $hook_suffix The current admin page.
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_admin_assets( string $hook_suffix ): void {
|
||||
// Only enqueue on FediStream pages.
|
||||
$screen = get_current_screen();
|
||||
if ( ! $screen ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fedistream_screens = array(
|
||||
'toplevel_page_fedistream',
|
||||
'fedistream_page_fedistream-settings',
|
||||
'fedistream_artist',
|
||||
'fedistream_album',
|
||||
'fedistream_track',
|
||||
'fedistream_playlist',
|
||||
'edit-fedistream_artist',
|
||||
'edit-fedistream_album',
|
||||
'edit-fedistream_track',
|
||||
'edit-fedistream_playlist',
|
||||
'edit-fedistream_genre',
|
||||
'edit-fedistream_mood',
|
||||
'edit-fedistream_license',
|
||||
);
|
||||
|
||||
if ( ! in_array( $screen->id, $fedistream_screens, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'wp-fedistream-admin',
|
||||
WP_FEDISTREAM_URL . 'assets/css/admin.css',
|
||||
array(),
|
||||
WP_FEDISTREAM_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wp-fedistream-admin',
|
||||
WP_FEDISTREAM_URL . 'assets/js/admin.js',
|
||||
array( 'jquery', 'jquery-ui-sortable' ),
|
||||
WP_FEDISTREAM_VERSION,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend assets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_frontend_assets(): void {
|
||||
// Always enqueue as shortcodes/widgets can be used anywhere.
|
||||
// Assets are lightweight and properly cached.
|
||||
wp_enqueue_style(
|
||||
'wp-fedistream',
|
||||
WP_FEDISTREAM_URL . 'assets/css/frontend.css',
|
||||
array(),
|
||||
WP_FEDISTREAM_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wp-fedistream',
|
||||
WP_FEDISTREAM_URL . 'assets/js/frontend.js',
|
||||
array(),
|
||||
WP_FEDISTREAM_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'wp-fedistream',
|
||||
'wpFediStream',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'wp-fedistream-nonce' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Twig environment.
|
||||
*
|
||||
* @return \Twig\Environment
|
||||
*/
|
||||
public function get_twig(): \Twig\Environment {
|
||||
return $this->twig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a Twig template.
|
||||
*
|
||||
* @param string $template Template name (without .twig extension).
|
||||
* @param array $context Template context variables.
|
||||
* @return string Rendered template.
|
||||
*/
|
||||
public function render( string $template, array $context = array() ): string {
|
||||
return $this->twig->render( $template . '.twig', $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a post type instance.
|
||||
*
|
||||
* @param string $name Post type name.
|
||||
* @return object|null Post type instance or null.
|
||||
*/
|
||||
public function get_post_type( string $name ): ?object {
|
||||
return $this->post_types[ $name ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a taxonomy instance.
|
||||
*
|
||||
* @param string $name Taxonomy name.
|
||||
* @return object|null Taxonomy instance or null.
|
||||
*/
|
||||
public function get_taxonomy( string $name ): ?object {
|
||||
return $this->taxonomies[ $name ] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_woocommerce_active(): bool {
|
||||
return class_exists( 'WooCommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ActivityPub plugin is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_activitypub_active(): bool {
|
||||
return class_exists( 'Activitypub\Activitypub' );
|
||||
}
|
||||
}
|
||||
202
includes/PostTypes/AbstractPostType.php
Normal file
202
includes/PostTypes/AbstractPostType.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract base class for custom post types.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract post type class.
|
||||
*
|
||||
* Provides common functionality for all custom post types.
|
||||
*/
|
||||
abstract class AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'register' ) );
|
||||
add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
|
||||
add_action( 'save_post_' . $this->post_type, array( $this, 'save_meta' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function register(): void;
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function add_meta_boxes(): void;
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
abstract public function save_meta( int $post_id, \WP_Post $post ): void;
|
||||
|
||||
/**
|
||||
* Get the post type key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_post_type(): string {
|
||||
return $this->post_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify nonce and user capabilities before saving.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $nonce_action Nonce action name.
|
||||
* @param string $nonce_name Nonce field name.
|
||||
* @return bool Whether save should proceed.
|
||||
*/
|
||||
protected function can_save( int $post_id, string $nonce_action, string $nonce_name ): bool {
|
||||
// Verify nonce.
|
||||
if ( ! isset( $_POST[ $nonce_name ] ) || ! wp_verify_nonce( sanitize_key( $_POST[ $nonce_name ] ), $nonce_action ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check autosave.
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check permissions.
|
||||
if ( ! current_user_can( 'edit_post', $post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a text meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_text_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a textarea meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_textarea_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, sanitize_textarea_field( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save an integer meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_int_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, absint( $_POST[ $post_key ] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a URL meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_url_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) ) {
|
||||
update_post_meta( $post_id, $meta_key, esc_url_raw( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a boolean meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_bool_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
$value = isset( $_POST[ $post_key ] ) ? 1 : 0;
|
||||
update_post_meta( $post_id, $meta_key, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save an array meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_array_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) && is_array( $_POST[ $post_key ] ) ) {
|
||||
$values = array_map( 'sanitize_text_field', wp_unslash( $_POST[ $post_key ] ) );
|
||||
update_post_meta( $post_id, $meta_key, $values );
|
||||
} else {
|
||||
delete_post_meta( $post_id, $meta_key );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize and save a date meta field.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $meta_key Meta key.
|
||||
* @param string $post_key POST array key.
|
||||
* @return void
|
||||
*/
|
||||
protected function save_date_meta( int $post_id, string $meta_key, string $post_key ): void {
|
||||
if ( isset( $_POST[ $post_key ] ) && ! empty( $_POST[ $post_key ] ) ) {
|
||||
$date = sanitize_text_field( wp_unslash( $_POST[ $post_key ] ) );
|
||||
// Validate date format (YYYY-MM-DD).
|
||||
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
|
||||
update_post_meta( $post_id, $meta_key, $date );
|
||||
}
|
||||
} else {
|
||||
delete_post_meta( $post_id, $meta_key );
|
||||
}
|
||||
}
|
||||
}
|
||||
340
includes/PostTypes/Album.php
Normal file
340
includes/PostTypes/Album.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
/**
|
||||
* Album custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Album post type class.
|
||||
*
|
||||
* Handles registration and management of albums/releases.
|
||||
*/
|
||||
class Album extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_album';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_album_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Albums', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Album', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Albums', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Album', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Album', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Album', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Album', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Album', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Albums', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Albums', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Albums:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No albums found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No albums found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Album Artwork', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set album artwork', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove album artwork', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as album artwork', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Album archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into album', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this album', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter albums list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Albums list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Albums list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'albums' ),
|
||||
'capability_type' => array( 'fedistream_album', 'fedistream_albums' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-album',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'albums',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_album_info',
|
||||
__( 'Album Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_album_artist',
|
||||
__( 'Artist', 'wp-fedistream' ),
|
||||
array( $this, 'render_artist_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_album_codes',
|
||||
__( 'Album Codes', 'wp-fedistream' ),
|
||||
array( $this, 'render_codes_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_album_save', 'fedistream_album_nonce' );
|
||||
|
||||
$album_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
$release_date = get_post_meta( $post->ID, self::META_PREFIX . 'release_date', true );
|
||||
$total_tracks = get_post_meta( $post->ID, self::META_PREFIX . 'total_tracks', true );
|
||||
$total_duration = get_post_meta( $post->ID, self::META_PREFIX . 'total_duration', true );
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_type"><?php esc_html_e( 'Release Type', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_album_type" id="fedistream_album_type">
|
||||
<option value="album" <?php selected( $album_type, 'album' ); ?>><?php esc_html_e( 'Album', 'wp-fedistream' ); ?></option>
|
||||
<option value="ep" <?php selected( $album_type, 'ep' ); ?>><?php esc_html_e( 'EP', 'wp-fedistream' ); ?></option>
|
||||
<option value="single" <?php selected( $album_type, 'single' ); ?>><?php esc_html_e( 'Single', 'wp-fedistream' ); ?></option>
|
||||
<option value="compilation" <?php selected( $album_type, 'compilation' ); ?>><?php esc_html_e( 'Compilation', 'wp-fedistream' ); ?></option>
|
||||
<option value="live" <?php selected( $album_type, 'live' ); ?>><?php esc_html_e( 'Live Album', 'wp-fedistream' ); ?></option>
|
||||
<option value="remix" <?php selected( $album_type, 'remix' ); ?>><?php esc_html_e( 'Remix Album', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_release_date"><?php esc_html_e( 'Release Date', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="date" name="fedistream_album_release_date" id="fedistream_album_release_date" value="<?php echo esc_attr( $release_date ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_total_tracks"><?php esc_html_e( 'Total Tracks', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_album_total_tracks" id="fedistream_album_total_tracks" value="<?php echo esc_attr( $total_tracks ); ?>" min="1" max="999" class="small-text">
|
||||
<p class="description"><?php esc_html_e( 'Auto-calculated when tracks are added.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_album_total_duration"><?php esc_html_e( 'Total Duration', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_album_total_duration" id="fedistream_album_total_duration" value="<?php echo esc_attr( $total_duration ); ?>" min="0" class="small-text" readonly>
|
||||
<span class="description"><?php esc_html_e( 'seconds (auto-calculated)', 'wp-fedistream' ); ?></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artist selection meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_artist_meta_box( \WP_Post $post ): void {
|
||||
$selected_artist = get_post_meta( $post->ID, self::META_PREFIX . 'artist', true );
|
||||
|
||||
$artists = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_album_artist"><?php esc_html_e( 'Primary Artist', 'wp-fedistream' ); ?></label>
|
||||
</p>
|
||||
<select name="fedistream_album_artist" id="fedistream_album_artist" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— Select Artist —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $artists as $artist ) : ?>
|
||||
<option value="<?php echo esc_attr( $artist->ID ); ?>" <?php selected( $selected_artist, $artist->ID ); ?>>
|
||||
<?php echo esc_html( $artist->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php
|
||||
if ( empty( $artists ) ) {
|
||||
printf(
|
||||
/* translators: %s: URL to add new artist */
|
||||
esc_html__( 'No artists found. %s', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( admin_url( 'post-new.php?post_type=fedistream_artist' ) ) . '">' . esc_html__( 'Add an artist first.', 'wp-fedistream' ) . '</a>'
|
||||
);
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album codes meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_codes_meta_box( \WP_Post $post ): void {
|
||||
$upc = get_post_meta( $post->ID, self::META_PREFIX . 'upc', true );
|
||||
$catalog_number = get_post_meta( $post->ID, self::META_PREFIX . 'catalog_number', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_album_upc"><?php esc_html_e( 'UPC/EAN', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_album_upc" id="fedistream_album_upc" value="<?php echo esc_attr( $upc ); ?>" class="widefat" pattern="[0-9]{12,13}" title="<?php esc_attr_e( '12-13 digit UPC/EAN code', 'wp-fedistream' ); ?>">
|
||||
</p>
|
||||
<p>
|
||||
<label for="fedistream_album_catalog_number"><?php esc_html_e( 'Catalog Number', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_album_catalog_number" id="fedistream_album_catalog_number" value="<?php echo esc_attr( $catalog_number ); ?>" class="widefat">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_album_save', 'fedistream_album_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save album type.
|
||||
if ( isset( $_POST['fedistream_album_type'] ) ) {
|
||||
$allowed_types = array( 'album', 'ep', 'single', 'compilation', 'live', 'remix' );
|
||||
$type = sanitize_text_field( wp_unslash( $_POST['fedistream_album_type'] ) );
|
||||
if ( in_array( $type, $allowed_types, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'type', $type );
|
||||
}
|
||||
}
|
||||
|
||||
// Save other fields.
|
||||
$this->save_date_meta( $post_id, self::META_PREFIX . 'release_date', 'fedistream_album_release_date' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'artist', 'fedistream_album_artist' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'total_tracks', 'fedistream_album_total_tracks' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'upc', 'fedistream_album_upc' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'catalog_number', 'fedistream_album_catalog_number' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Album data or null.
|
||||
*/
|
||||
public static function get_album( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_album' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$artist_id = get_post_meta( $post_id, self::META_PREFIX . 'artist', true );
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'description' => $post->post_content,
|
||||
'excerpt' => $post->post_excerpt,
|
||||
'type' => get_post_meta( $post_id, self::META_PREFIX . 'type', true ) ?: 'album',
|
||||
'release_date' => get_post_meta( $post_id, self::META_PREFIX . 'release_date', true ),
|
||||
'artist_id' => $artist_id,
|
||||
'artist_name' => $artist_id ? get_the_title( $artist_id ) : '',
|
||||
'total_tracks' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_tracks', true ),
|
||||
'total_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_duration', true ),
|
||||
'upc' => get_post_meta( $post_id, self::META_PREFIX . 'upc', true ),
|
||||
'catalog_number' => get_post_meta( $post_id, self::META_PREFIX . 'catalog_number', true ),
|
||||
'artwork' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update album track count and duration.
|
||||
*
|
||||
* @param int $album_id Album post ID.
|
||||
* @return void
|
||||
*/
|
||||
public static function update_album_stats( int $album_id ): void {
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_key' => '_fedistream_track_album',
|
||||
'meta_value' => $album_id,
|
||||
)
|
||||
);
|
||||
|
||||
$total_tracks = count( $tracks );
|
||||
$total_duration = 0;
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$duration = (int) get_post_meta( $track->ID, '_fedistream_track_duration', true );
|
||||
$total_duration += $duration;
|
||||
}
|
||||
|
||||
update_post_meta( $album_id, self::META_PREFIX . 'total_tracks', $total_tracks );
|
||||
update_post_meta( $album_id, self::META_PREFIX . 'total_duration', $total_duration );
|
||||
}
|
||||
}
|
||||
331
includes/PostTypes/Artist.php
Normal file
331
includes/PostTypes/Artist.php
Normal file
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
/**
|
||||
* Artist custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Artist post type class.
|
||||
*
|
||||
* Handles registration and management of artist/band profiles.
|
||||
*/
|
||||
class Artist extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_artist';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_artist_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Artists', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Artist', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Artists', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Artist', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Artist', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Artist', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Artist', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Artist', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Artists', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Artists', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Artists:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No artists found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No artists found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Artist Photo', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set artist photo', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove artist photo', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as artist photo', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Artist archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into artist', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this artist', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter artists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Artists list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Artists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'artists' ),
|
||||
'capability_type' => array( 'fedistream_artist', 'fedistream_artists' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-groups',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'artists',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_artist_info',
|
||||
__( 'Artist Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_artist_social',
|
||||
__( 'Social Links', 'wp-fedistream' ),
|
||||
array( $this, 'render_social_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_artist_members',
|
||||
__( 'Band Members', 'wp-fedistream' ),
|
||||
array( $this, 'render_members_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artist info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_artist_save', 'fedistream_artist_nonce' );
|
||||
|
||||
$artist_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
$formed_date = get_post_meta( $post->ID, self::META_PREFIX . 'formed_date', true );
|
||||
$location = get_post_meta( $post->ID, self::META_PREFIX . 'location', true );
|
||||
$website = get_post_meta( $post->ID, self::META_PREFIX . 'website', true );
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_type"><?php esc_html_e( 'Artist Type', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_artist_type" id="fedistream_artist_type">
|
||||
<option value="solo" <?php selected( $artist_type, 'solo' ); ?>><?php esc_html_e( 'Solo Artist', 'wp-fedistream' ); ?></option>
|
||||
<option value="band" <?php selected( $artist_type, 'band' ); ?>><?php esc_html_e( 'Band', 'wp-fedistream' ); ?></option>
|
||||
<option value="duo" <?php selected( $artist_type, 'duo' ); ?>><?php esc_html_e( 'Duo', 'wp-fedistream' ); ?></option>
|
||||
<option value="collective" <?php selected( $artist_type, 'collective' ); ?>><?php esc_html_e( 'Collective', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_formed_date"><?php esc_html_e( 'Formed Date', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="date" name="fedistream_artist_formed_date" id="fedistream_artist_formed_date" value="<?php echo esc_attr( $formed_date ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'When the artist/band was formed or started their career.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_location"><?php esc_html_e( 'Location', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" name="fedistream_artist_location" id="fedistream_artist_location" value="<?php echo esc_attr( $location ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'City, Country or region where the artist is based.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_artist_website"><?php esc_html_e( 'Website', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="fedistream_artist_website" id="fedistream_artist_website" value="<?php echo esc_url( $website ); ?>" class="regular-text">
|
||||
<p class="description"><?php esc_html_e( 'Official website URL.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render social links meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_social_meta_box( \WP_Post $post ): void {
|
||||
$social_links = get_post_meta( $post->ID, self::META_PREFIX . 'social_links', true );
|
||||
if ( ! is_array( $social_links ) ) {
|
||||
$social_links = array();
|
||||
}
|
||||
|
||||
$platforms = array(
|
||||
'mastodon' => __( 'Mastodon', 'wp-fedistream' ),
|
||||
'bandcamp' => __( 'Bandcamp', 'wp-fedistream' ),
|
||||
'soundcloud' => __( 'SoundCloud', 'wp-fedistream' ),
|
||||
'youtube' => __( 'YouTube', 'wp-fedistream' ),
|
||||
'instagram' => __( 'Instagram', 'wp-fedistream' ),
|
||||
'twitter' => __( 'Twitter/X', 'wp-fedistream' ),
|
||||
'facebook' => __( 'Facebook', 'wp-fedistream' ),
|
||||
'tiktok' => __( 'TikTok', 'wp-fedistream' ),
|
||||
'other' => __( 'Other', 'wp-fedistream' ),
|
||||
);
|
||||
?>
|
||||
<table class="form-table">
|
||||
<?php foreach ( $platforms as $key => $label ) : ?>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_social_<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $label ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="url" name="fedistream_artist_social[<?php echo esc_attr( $key ); ?>]" id="fedistream_social_<?php echo esc_attr( $key ); ?>" value="<?php echo esc_url( $social_links[ $key ] ?? '' ); ?>" class="regular-text">
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render band members meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_members_meta_box( \WP_Post $post ): void {
|
||||
$members = get_post_meta( $post->ID, self::META_PREFIX . 'members', true );
|
||||
if ( ! is_array( $members ) ) {
|
||||
$members = array();
|
||||
}
|
||||
|
||||
$artist_type = get_post_meta( $post->ID, self::META_PREFIX . 'type', true );
|
||||
?>
|
||||
<div id="fedistream-members-wrapper" style="<?php echo 'solo' === $artist_type ? 'display:none;' : ''; ?>">
|
||||
<p class="description"><?php esc_html_e( 'Add band/group members (comma-separated names).', 'wp-fedistream' ); ?></p>
|
||||
<textarea name="fedistream_artist_members" id="fedistream_artist_members" rows="5" class="large-text"><?php echo esc_textarea( implode( "\n", $members ) ); ?></textarea>
|
||||
<p class="description"><?php esc_html_e( 'One member per line.', 'wp-fedistream' ); ?></p>
|
||||
</div>
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
$('#fedistream_artist_type').on('change', function() {
|
||||
if ($(this).val() === 'solo') {
|
||||
$('#fedistream-members-wrapper').hide();
|
||||
} else {
|
||||
$('#fedistream-members-wrapper').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_artist_save', 'fedistream_artist_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save artist type.
|
||||
if ( isset( $_POST['fedistream_artist_type'] ) ) {
|
||||
$allowed_types = array( 'solo', 'band', 'duo', 'collective' );
|
||||
$type = sanitize_text_field( wp_unslash( $_POST['fedistream_artist_type'] ) );
|
||||
if ( in_array( $type, $allowed_types, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'type', $type );
|
||||
}
|
||||
}
|
||||
|
||||
// Save other fields.
|
||||
$this->save_date_meta( $post_id, self::META_PREFIX . 'formed_date', 'fedistream_artist_formed_date' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'location', 'fedistream_artist_location' );
|
||||
$this->save_url_meta( $post_id, self::META_PREFIX . 'website', 'fedistream_artist_website' );
|
||||
|
||||
// Save social links.
|
||||
if ( isset( $_POST['fedistream_artist_social'] ) && is_array( $_POST['fedistream_artist_social'] ) ) {
|
||||
$social_links = array();
|
||||
foreach ( $_POST['fedistream_artist_social'] as $key => $url ) {
|
||||
$clean_key = sanitize_key( $key );
|
||||
$clean_url = esc_url_raw( wp_unslash( $url ) );
|
||||
if ( ! empty( $clean_url ) ) {
|
||||
$social_links[ $clean_key ] = $clean_url;
|
||||
}
|
||||
}
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'social_links', $social_links );
|
||||
}
|
||||
|
||||
// Save members.
|
||||
if ( isset( $_POST['fedistream_artist_members'] ) ) {
|
||||
$members_text = sanitize_textarea_field( wp_unslash( $_POST['fedistream_artist_members'] ) );
|
||||
$members = array_filter( array_map( 'trim', explode( "\n", $members_text ) ) );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'members', $members );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Artist data or null.
|
||||
*/
|
||||
public static function get_artist( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_artist' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'name' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'bio' => $post->post_content,
|
||||
'excerpt' => $post->post_excerpt,
|
||||
'type' => get_post_meta( $post_id, self::META_PREFIX . 'type', true ) ?: 'solo',
|
||||
'formed_date' => get_post_meta( $post_id, self::META_PREFIX . 'formed_date', true ),
|
||||
'location' => get_post_meta( $post_id, self::META_PREFIX . 'location', true ),
|
||||
'website' => get_post_meta( $post_id, self::META_PREFIX . 'website', true ),
|
||||
'social_links' => get_post_meta( $post_id, self::META_PREFIX . 'social_links', true ) ?: array(),
|
||||
'members' => get_post_meta( $post_id, self::META_PREFIX . 'members', true ) ?: array(),
|
||||
'photo' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
}
|
||||
458
includes/PostTypes/Playlist.php
Normal file
458
includes/PostTypes/Playlist.php
Normal file
@@ -0,0 +1,458 @@
|
||||
<?php
|
||||
/**
|
||||
* Playlist custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playlist post type class.
|
||||
*
|
||||
* Handles registration and management of playlists.
|
||||
*/
|
||||
class Playlist extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_playlist';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_playlist_';
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Playlists', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Playlist', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Playlists', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Playlist', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Playlist', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Playlist', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Playlist', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Playlist', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Playlists', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Playlists', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Playlists:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No playlists found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No playlists found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Playlist Cover', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set playlist cover', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove playlist cover', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as playlist cover', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Playlist archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into playlist', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this playlist', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter playlists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Playlists list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Playlists list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'playlists' ),
|
||||
'capability_type' => array( 'fedistream_playlist', 'fedistream_playlists' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-playlist-audio',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'author', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'playlists',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_playlist_tracks',
|
||||
__( 'Playlist Tracks', 'wp-fedistream' ),
|
||||
array( $this, 'render_tracks_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_playlist_settings',
|
||||
__( 'Playlist Settings', 'wp-fedistream' ),
|
||||
array( $this, 'render_settings_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_playlist_stats',
|
||||
__( 'Playlist Stats', 'wp-fedistream' ),
|
||||
array( $this, 'render_stats_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist tracks meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_tracks_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_playlist_save', 'fedistream_playlist_nonce' );
|
||||
|
||||
global $wpdb;
|
||||
|
||||
// Get tracks in this playlist from the pivot table.
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$playlist_tracks = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id, position FROM $table WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$post->ID
|
||||
)
|
||||
);
|
||||
|
||||
$track_ids = wp_list_pluck( $playlist_tracks, 'track_id' );
|
||||
|
||||
// Get all available tracks.
|
||||
$available_tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<div class="fedistream-playlist-tracks">
|
||||
<h4><?php esc_html_e( 'Current Tracks', 'wp-fedistream' ); ?></h4>
|
||||
<ul id="fedistream-playlist-track-list" class="fedistream-sortable">
|
||||
<?php
|
||||
foreach ( $track_ids as $track_id ) :
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track ) {
|
||||
continue;
|
||||
}
|
||||
$artists = get_post_meta( $track_id, '_fedistream_track_artists', true ) ?: array();
|
||||
$duration = get_post_meta( $track_id, '_fedistream_track_duration', true );
|
||||
?>
|
||||
<li data-track-id="<?php echo esc_attr( $track_id ); ?>" style="padding: 8px; margin: 4px 0; background: #f9f9f9; border: 1px solid #ddd; cursor: move;">
|
||||
<input type="hidden" name="fedistream_playlist_tracks[]" value="<?php echo esc_attr( $track_id ); ?>">
|
||||
<span class="dashicons dashicons-menu" style="vertical-align: middle;"></span>
|
||||
<strong><?php echo esc_html( $track->post_title ); ?></strong>
|
||||
<?php if ( ! empty( $artists ) ) : ?>
|
||||
<span class="description">
|
||||
— <?php echo esc_html( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="description">(<?php echo esc_html( gmdate( 'i:s', (int) $duration ) ); ?>)</span>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="button-link fedistream-remove-track" style="color: #a00; float: right;">
|
||||
<?php esc_html_e( 'Remove', 'wp-fedistream' ); ?>
|
||||
</button>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4><?php esc_html_e( 'Add Tracks', 'wp-fedistream' ); ?></h4>
|
||||
<p>
|
||||
<select id="fedistream-add-track-select" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— Select a track to add —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $available_tracks as $track ) : ?>
|
||||
<?php
|
||||
$artists = get_post_meta( $track->ID, '_fedistream_track_artists', true ) ?: array();
|
||||
$duration = get_post_meta( $track->ID, '_fedistream_track_duration', true );
|
||||
?>
|
||||
<option value="<?php echo esc_attr( $track->ID ); ?>" data-title="<?php echo esc_attr( $track->post_title ); ?>" data-artists="<?php echo esc_attr( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>" data-duration="<?php echo esc_attr( $duration ? gmdate( 'i:s', (int) $duration ) : '' ); ?>">
|
||||
<?php echo esc_html( $track->post_title ); ?>
|
||||
<?php if ( ! empty( $artists ) ) : ?>
|
||||
— <?php echo esc_html( implode( ', ', array_map( 'get_the_title', $artists ) ) ); ?>
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<button type="button" class="button" id="fedistream-add-track-btn"><?php esc_html_e( 'Add to Playlist', 'wp-fedistream' ); ?></button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.fedistream-sortable li { list-style: none; }
|
||||
.fedistream-sortable .ui-sortable-helper { background: #fff !important; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
|
||||
.fedistream-sortable .ui-sortable-placeholder { visibility: visible !important; background: #e0e0e0; border: 1px dashed #999; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
// Make list sortable.
|
||||
$('#fedistream-playlist-track-list').sortable({
|
||||
placeholder: 'ui-sortable-placeholder',
|
||||
axis: 'y'
|
||||
});
|
||||
|
||||
// Add track.
|
||||
$('#fedistream-add-track-btn').on('click', function() {
|
||||
var $select = $('#fedistream-add-track-select');
|
||||
var trackId = $select.val();
|
||||
if (!trackId) return;
|
||||
|
||||
var $option = $select.find('option:selected');
|
||||
var title = $option.data('title');
|
||||
var artists = $option.data('artists');
|
||||
var duration = $option.data('duration');
|
||||
|
||||
var html = '<li data-track-id="' + trackId + '" style="padding: 8px; margin: 4px 0; background: #f9f9f9; border: 1px solid #ddd; cursor: move;">' +
|
||||
'<input type="hidden" name="fedistream_playlist_tracks[]" value="' + trackId + '">' +
|
||||
'<span class="dashicons dashicons-menu" style="vertical-align: middle;"></span> ' +
|
||||
'<strong>' + title + '</strong>';
|
||||
|
||||
if (artists) {
|
||||
html += ' <span class="description">— ' + artists + '</span>';
|
||||
}
|
||||
if (duration) {
|
||||
html += ' <span class="description">(' + duration + ')</span>';
|
||||
}
|
||||
|
||||
html += '<button type="button" class="button-link fedistream-remove-track" style="color: #a00; float: right;"><?php echo esc_js( __( 'Remove', 'wp-fedistream' ) ); ?></button>' +
|
||||
'</li>';
|
||||
|
||||
$('#fedistream-playlist-track-list').append(html);
|
||||
$select.val('');
|
||||
});
|
||||
|
||||
// Remove track.
|
||||
$(document).on('click', '.fedistream-remove-track', function() {
|
||||
$(this).closest('li').remove();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist settings meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_settings_meta_box( \WP_Post $post ): void {
|
||||
$visibility = get_post_meta( $post->ID, self::META_PREFIX . 'visibility', true ) ?: 'public';
|
||||
$collaborative = get_post_meta( $post->ID, self::META_PREFIX . 'collaborative', true );
|
||||
$federated = get_post_meta( $post->ID, self::META_PREFIX . 'federated', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_playlist_visibility"><?php esc_html_e( 'Visibility', 'wp-fedistream' ); ?></label>
|
||||
<select name="fedistream_playlist_visibility" id="fedistream_playlist_visibility" class="widefat">
|
||||
<option value="public" <?php selected( $visibility, 'public' ); ?>><?php esc_html_e( 'Public', 'wp-fedistream' ); ?></option>
|
||||
<option value="unlisted" <?php selected( $visibility, 'unlisted' ); ?>><?php esc_html_e( 'Unlisted (link only)', 'wp-fedistream' ); ?></option>
|
||||
<option value="private" <?php selected( $visibility, 'private' ); ?>><?php esc_html_e( 'Private', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_playlist_collaborative" value="1" <?php checked( $collaborative, 1 ); ?>>
|
||||
<?php esc_html_e( 'Collaborative', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<span class="description"><?php esc_html_e( 'Allow others to add tracks.', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_playlist_federated" value="1" <?php checked( $federated, 1 ); ?>>
|
||||
<?php esc_html_e( 'Federated', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<span class="description"><?php esc_html_e( 'Allow tracks from other FediStream instances.', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render playlist stats meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_stats_meta_box( \WP_Post $post ): void {
|
||||
$track_count = get_post_meta( $post->ID, self::META_PREFIX . 'track_count', true ) ?: 0;
|
||||
$total_duration = get_post_meta( $post->ID, self::META_PREFIX . 'total_duration', true ) ?: 0;
|
||||
?>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Tracks:', 'wp-fedistream' ); ?></strong>
|
||||
<?php echo esc_html( $track_count ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Total Duration:', 'wp-fedistream' ); ?></strong>
|
||||
<?php
|
||||
if ( $total_duration > 3600 ) {
|
||||
echo esc_html( gmdate( 'H:i:s', (int) $total_duration ) );
|
||||
} else {
|
||||
echo esc_html( gmdate( 'i:s', (int) $total_duration ) );
|
||||
}
|
||||
?>
|
||||
</p>
|
||||
<p class="description"><?php esc_html_e( 'Stats are automatically updated when saved.', 'wp-fedistream' ); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_playlist_save', 'fedistream_playlist_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
|
||||
// Save visibility.
|
||||
if ( isset( $_POST['fedistream_playlist_visibility'] ) ) {
|
||||
$allowed_visibility = array( 'public', 'unlisted', 'private' );
|
||||
$visibility = sanitize_text_field( wp_unslash( $_POST['fedistream_playlist_visibility'] ) );
|
||||
if ( in_array( $visibility, $allowed_visibility, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'visibility', $visibility );
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings.
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'collaborative', 'fedistream_playlist_collaborative' );
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'federated', 'fedistream_playlist_federated' );
|
||||
|
||||
// Save tracks.
|
||||
// First, delete existing tracks for this playlist.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$wpdb->delete( $table, array( 'playlist_id' => $post_id ), array( '%d' ) );
|
||||
|
||||
// Insert new tracks.
|
||||
if ( isset( $_POST['fedistream_playlist_tracks'] ) && is_array( $_POST['fedistream_playlist_tracks'] ) ) {
|
||||
$position = 0;
|
||||
$total_duration = 0;
|
||||
|
||||
foreach ( $_POST['fedistream_playlist_tracks'] as $track_id ) {
|
||||
$track_id = absint( $track_id );
|
||||
if ( $track_id > 0 ) {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'playlist_id' => $post_id,
|
||||
'track_id' => $track_id,
|
||||
'position' => $position,
|
||||
),
|
||||
array( '%d', '%d', '%d' )
|
||||
);
|
||||
++$position;
|
||||
|
||||
// Sum duration.
|
||||
$duration = (int) get_post_meta( $track_id, '_fedistream_track_duration', true );
|
||||
$total_duration += $duration;
|
||||
}
|
||||
}
|
||||
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'track_count', $position );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'total_duration', $total_duration );
|
||||
} else {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'track_count', 0 );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'total_duration', 0 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get playlist by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Playlist data or null.
|
||||
*/
|
||||
public static function get_playlist( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_playlist' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'description' => $post->post_content,
|
||||
'author_id' => $post->post_author,
|
||||
'author_name' => get_the_author_meta( 'display_name', $post->post_author ),
|
||||
'visibility' => get_post_meta( $post_id, self::META_PREFIX . 'visibility', true ) ?: 'public',
|
||||
'collaborative' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'collaborative', true ),
|
||||
'federated' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'federated', true ),
|
||||
'track_count' => (int) get_post_meta( $post_id, self::META_PREFIX . 'track_count', true ),
|
||||
'total_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'total_duration', true ),
|
||||
'cover' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracks in a playlist.
|
||||
*
|
||||
* @param int $playlist_id Playlist post ID.
|
||||
* @return array Array of track data.
|
||||
*/
|
||||
public static function get_playlist_tracks( int $playlist_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_playlist_tracks';
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id FROM $table WHERE playlist_id = %d ORDER BY position ASC",
|
||||
$playlist_id
|
||||
)
|
||||
);
|
||||
|
||||
$tracks = array();
|
||||
foreach ( $results as $row ) {
|
||||
$track = Track::get_track( (int) $row->track_id );
|
||||
if ( $track ) {
|
||||
$tracks[] = $track;
|
||||
}
|
||||
}
|
||||
|
||||
return $tracks;
|
||||
}
|
||||
}
|
||||
615
includes/PostTypes/Track.php
Normal file
615
includes/PostTypes/Track.php
Normal file
@@ -0,0 +1,615 @@
|
||||
<?php
|
||||
/**
|
||||
* Track custom post type.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\PostTypes;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track post type class.
|
||||
*
|
||||
* Handles registration and management of individual tracks.
|
||||
*/
|
||||
class Track extends AbstractPostType {
|
||||
|
||||
/**
|
||||
* Post type key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $post_type = 'fedistream_track';
|
||||
|
||||
/**
|
||||
* Meta key prefix.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private const META_PREFIX = '_fedistream_track_';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
add_action( 'save_post_fedistream_track', array( $this, 'update_album_on_save' ), 20, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the post type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Tracks', 'Post type general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Track', 'Post type singular name', 'wp-fedistream' ),
|
||||
'menu_name' => _x( 'Tracks', 'Admin Menu text', 'wp-fedistream' ),
|
||||
'name_admin_bar' => _x( 'Track', 'Add New on Toolbar', 'wp-fedistream' ),
|
||||
'add_new' => __( 'Add New', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Track', 'wp-fedistream' ),
|
||||
'new_item' => __( 'New Track', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Track', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Track', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Tracks', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Tracks', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Tracks:', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No tracks found.', 'wp-fedistream' ),
|
||||
'not_found_in_trash' => __( 'No tracks found in Trash.', 'wp-fedistream' ),
|
||||
'featured_image' => _x( 'Track Artwork', 'Overrides the "Featured Image" phrase', 'wp-fedistream' ),
|
||||
'set_featured_image' => _x( 'Set track artwork', 'Overrides the "Set featured image" phrase', 'wp-fedistream' ),
|
||||
'remove_featured_image' => _x( 'Remove track artwork', 'Overrides the "Remove featured image" phrase', 'wp-fedistream' ),
|
||||
'use_featured_image' => _x( 'Use as track artwork', 'Overrides the "Use as featured image" phrase', 'wp-fedistream' ),
|
||||
'archives' => _x( 'Track archives', 'The post type archive label', 'wp-fedistream' ),
|
||||
'insert_into_item' => _x( 'Insert into track', 'Overrides the "Insert into post" phrase', 'wp-fedistream' ),
|
||||
'uploaded_to_this_item' => _x( 'Uploaded to this track', 'Overrides the "Uploaded to this post" phrase', 'wp-fedistream' ),
|
||||
'filter_items_list' => _x( 'Filter tracks list', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list_navigation' => _x( 'Tracks list navigation', 'Screen reader text', 'wp-fedistream' ),
|
||||
'items_list' => _x( 'Tracks list', 'Screen reader text', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'public' => true,
|
||||
'publicly_queryable' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'tracks' ),
|
||||
'capability_type' => array( 'fedistream_track', 'fedistream_tracks' ),
|
||||
'map_meta_cap' => true,
|
||||
'has_archive' => true,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'menu_icon' => 'dashicons-format-audio',
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'revisions' ),
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'tracks',
|
||||
);
|
||||
|
||||
register_post_type( $this->post_type, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add meta boxes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_meta_boxes(): void {
|
||||
add_meta_box(
|
||||
'fedistream_track_audio',
|
||||
__( 'Audio File', 'wp-fedistream' ),
|
||||
array( $this, 'render_audio_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_info',
|
||||
__( 'Track Information', 'wp-fedistream' ),
|
||||
array( $this, 'render_info_meta_box' ),
|
||||
$this->post_type,
|
||||
'normal',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_album',
|
||||
__( 'Album', 'wp-fedistream' ),
|
||||
array( $this, 'render_album_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'high'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_artists',
|
||||
__( 'Artists', 'wp-fedistream' ),
|
||||
array( $this, 'render_artists_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
|
||||
add_meta_box(
|
||||
'fedistream_track_codes',
|
||||
__( 'Track Codes', 'wp-fedistream' ),
|
||||
array( $this, 'render_codes_meta_box' ),
|
||||
$this->post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render audio file meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_audio_meta_box( \WP_Post $post ): void {
|
||||
wp_nonce_field( 'fedistream_track_save', 'fedistream_track_nonce' );
|
||||
|
||||
$audio_file = get_post_meta( $post->ID, self::META_PREFIX . 'audio_file', true );
|
||||
$audio_format = get_post_meta( $post->ID, self::META_PREFIX . 'audio_format', true );
|
||||
$duration = get_post_meta( $post->ID, self::META_PREFIX . 'duration', true );
|
||||
|
||||
wp_enqueue_media();
|
||||
?>
|
||||
<div class="fedistream-audio-upload">
|
||||
<p>
|
||||
<label for="fedistream_track_audio_file"><?php esc_html_e( 'Audio File', 'wp-fedistream' ); ?></label>
|
||||
</p>
|
||||
<input type="hidden" name="fedistream_track_audio_file" id="fedistream_track_audio_file" value="<?php echo esc_attr( $audio_file ); ?>">
|
||||
<div id="fedistream-audio-preview">
|
||||
<?php if ( $audio_file ) : ?>
|
||||
<?php
|
||||
$audio_url = wp_get_attachment_url( $audio_file );
|
||||
if ( $audio_url ) :
|
||||
?>
|
||||
<audio controls style="width: 100%;">
|
||||
<source src="<?php echo esc_url( $audio_url ); ?>" type="audio/<?php echo esc_attr( $audio_format ?: 'mpeg' ); ?>">
|
||||
</audio>
|
||||
<p><strong><?php echo esc_html( basename( get_attached_file( $audio_file ) ) ); ?></strong></p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p>
|
||||
<button type="button" class="button" id="fedistream-upload-audio"><?php esc_html_e( 'Select Audio File', 'wp-fedistream' ); ?></button>
|
||||
<button type="button" class="button" id="fedistream-remove-audio" style="<?php echo $audio_file ? '' : 'display:none;'; ?>"><?php esc_html_e( 'Remove', 'wp-fedistream' ); ?></button>
|
||||
</p>
|
||||
<p class="description"><?php esc_html_e( 'Supported formats: MP3, WAV, FLAC, OGG', 'wp-fedistream' ); ?></p>
|
||||
</div>
|
||||
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_audio_format"><?php esc_html_e( 'Audio Format', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_track_audio_format" id="fedistream_track_audio_format">
|
||||
<option value=""><?php esc_html_e( '— Auto-detect —', 'wp-fedistream' ); ?></option>
|
||||
<option value="mp3" <?php selected( $audio_format, 'mp3' ); ?>>MP3</option>
|
||||
<option value="wav" <?php selected( $audio_format, 'wav' ); ?>>WAV</option>
|
||||
<option value="flac" <?php selected( $audio_format, 'flac' ); ?>>FLAC</option>
|
||||
<option value="ogg" <?php selected( $audio_format, 'ogg' ); ?>>OGG</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_duration"><?php esc_html_e( 'Duration (seconds)', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_duration" id="fedistream_track_duration" value="<?php echo esc_attr( $duration ); ?>" min="0" class="small-text">
|
||||
<span id="fedistream-duration-display">
|
||||
<?php
|
||||
if ( $duration ) {
|
||||
printf(
|
||||
/* translators: %s: formatted duration */
|
||||
esc_html__( '(%s)', 'wp-fedistream' ),
|
||||
esc_html( gmdate( 'i:s', (int) $duration ) )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</span>
|
||||
<p class="description"><?php esc_html_e( 'Auto-detected from audio file if available.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
jQuery(document).ready(function($) {
|
||||
var mediaUploader;
|
||||
|
||||
$('#fedistream-upload-audio').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (mediaUploader) {
|
||||
mediaUploader.open();
|
||||
return;
|
||||
}
|
||||
|
||||
mediaUploader = wp.media({
|
||||
title: '<?php echo esc_js( __( 'Select Audio File', 'wp-fedistream' ) ); ?>',
|
||||
button: { text: '<?php echo esc_js( __( 'Use this file', 'wp-fedistream' ) ); ?>' },
|
||||
library: { type: 'audio' },
|
||||
multiple: false
|
||||
});
|
||||
|
||||
mediaUploader.on('select', function() {
|
||||
var attachment = mediaUploader.state().get('selection').first().toJSON();
|
||||
$('#fedistream_track_audio_file').val(attachment.id);
|
||||
$('#fedistream-audio-preview').html(
|
||||
'<audio controls style="width: 100%;"><source src="' + attachment.url + '" type="' + attachment.mime + '"></audio>' +
|
||||
'<p><strong>' + attachment.filename + '</strong></p>'
|
||||
);
|
||||
$('#fedistream-remove-audio').show();
|
||||
|
||||
// Auto-detect format.
|
||||
var ext = attachment.filename.split('.').pop().toLowerCase();
|
||||
if (['mp3', 'wav', 'flac', 'ogg'].indexOf(ext) !== -1) {
|
||||
$('#fedistream_track_audio_format').val(ext);
|
||||
}
|
||||
});
|
||||
|
||||
mediaUploader.open();
|
||||
});
|
||||
|
||||
$('#fedistream-remove-audio').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
$('#fedistream_track_audio_file').val('');
|
||||
$('#fedistream-audio-preview').html('');
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
// Duration display.
|
||||
$('#fedistream_track_duration').on('change', function() {
|
||||
var seconds = parseInt($(this).val(), 10);
|
||||
if (seconds > 0) {
|
||||
var mins = Math.floor(seconds / 60);
|
||||
var secs = seconds % 60;
|
||||
$('#fedistream-duration-display').text('(' + mins + ':' + (secs < 10 ? '0' : '') + secs + ')');
|
||||
} else {
|
||||
$('#fedistream-duration-display').text('');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track info meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_info_meta_box( \WP_Post $post ): void {
|
||||
$track_number = get_post_meta( $post->ID, self::META_PREFIX . 'number', true );
|
||||
$disc_number = get_post_meta( $post->ID, self::META_PREFIX . 'disc_number', true );
|
||||
$bpm = get_post_meta( $post->ID, self::META_PREFIX . 'bpm', true );
|
||||
$key = get_post_meta( $post->ID, self::META_PREFIX . 'key', true );
|
||||
$explicit = get_post_meta( $post->ID, self::META_PREFIX . 'explicit', true );
|
||||
$preview_start = get_post_meta( $post->ID, self::META_PREFIX . 'preview_start', true );
|
||||
$preview_duration = get_post_meta( $post->ID, self::META_PREFIX . 'preview_duration', true );
|
||||
|
||||
$musical_keys = array(
|
||||
'' => __( '— Select Key —', 'wp-fedistream' ),
|
||||
'C' => 'C Major',
|
||||
'Cm' => 'C Minor',
|
||||
'C#' => 'C# Major',
|
||||
'C#m' => 'C# Minor',
|
||||
'D' => 'D Major',
|
||||
'Dm' => 'D Minor',
|
||||
'D#' => 'D# Major',
|
||||
'D#m' => 'D# Minor',
|
||||
'E' => 'E Major',
|
||||
'Em' => 'E Minor',
|
||||
'F' => 'F Major',
|
||||
'Fm' => 'F Minor',
|
||||
'F#' => 'F# Major',
|
||||
'F#m' => 'F# Minor',
|
||||
'G' => 'G Major',
|
||||
'Gm' => 'G Minor',
|
||||
'G#' => 'G# Major',
|
||||
'G#m' => 'G# Minor',
|
||||
'A' => 'A Major',
|
||||
'Am' => 'A Minor',
|
||||
'A#' => 'A# Major',
|
||||
'A#m' => 'A# Minor',
|
||||
'B' => 'B Major',
|
||||
'Bm' => 'B Minor',
|
||||
);
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_number"><?php esc_html_e( 'Track Number', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_number" id="fedistream_track_number" value="<?php echo esc_attr( $track_number ); ?>" min="1" max="999" class="small-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_disc_number"><?php esc_html_e( 'Disc Number', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_disc_number" id="fedistream_track_disc_number" value="<?php echo esc_attr( $disc_number ?: 1 ); ?>" min="1" max="99" class="small-text">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_bpm"><?php esc_html_e( 'BPM', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" name="fedistream_track_bpm" id="fedistream_track_bpm" value="<?php echo esc_attr( $bpm ); ?>" min="20" max="300" class="small-text">
|
||||
<p class="description"><?php esc_html_e( 'Beats per minute (tempo).', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_key"><?php esc_html_e( 'Musical Key', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="fedistream_track_key" id="fedistream_track_key">
|
||||
<?php foreach ( $musical_keys as $value => $label ) : ?>
|
||||
<option value="<?php echo esc_attr( $value ); ?>" <?php selected( $key, $value ); ?>><?php echo esc_html( $label ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php esc_html_e( 'Explicit Content', 'wp-fedistream' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="fedistream_track_explicit" value="1" <?php checked( $explicit, 1 ); ?>>
|
||||
<?php esc_html_e( 'This track contains explicit content', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="fedistream_track_preview_start"><?php esc_html_e( 'Preview Settings', 'wp-fedistream' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<label>
|
||||
<?php esc_html_e( 'Start at:', 'wp-fedistream' ); ?>
|
||||
<input type="number" name="fedistream_track_preview_start" id="fedistream_track_preview_start" value="<?php echo esc_attr( $preview_start ?: 0 ); ?>" min="0" class="small-text">
|
||||
<?php esc_html_e( 'seconds', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
<?php esc_html_e( 'Duration:', 'wp-fedistream' ); ?>
|
||||
<input type="number" name="fedistream_track_preview_duration" id="fedistream_track_preview_duration" value="<?php echo esc_attr( $preview_duration ?: 30 ); ?>" min="10" max="60" class="small-text">
|
||||
<?php esc_html_e( 'seconds', 'wp-fedistream' ); ?>
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e( 'Preview clip for non-authenticated users or before purchase.', 'wp-fedistream' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album selection meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_album_meta_box( \WP_Post $post ): void {
|
||||
$selected_album = get_post_meta( $post->ID, self::META_PREFIX . 'album', true );
|
||||
|
||||
$albums = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => array( 'publish', 'draft' ),
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p>
|
||||
<select name="fedistream_track_album" id="fedistream_track_album" class="widefat">
|
||||
<option value=""><?php esc_html_e( '— No Album (Single) —', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $albums as $album ) : ?>
|
||||
<?php
|
||||
$artist_id = get_post_meta( $album->ID, '_fedistream_album_artist', true );
|
||||
$artist_name = $artist_id ? get_the_title( $artist_id ) : '';
|
||||
?>
|
||||
<option value="<?php echo esc_attr( $album->ID ); ?>" <?php selected( $selected_album, $album->ID ); ?>>
|
||||
<?php echo esc_html( $album->post_title ); ?>
|
||||
<?php if ( $artist_name ) : ?>
|
||||
(<?php echo esc_html( $artist_name ); ?>)
|
||||
<?php endif; ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render artists meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_artists_meta_box( \WP_Post $post ): void {
|
||||
$selected_artists = get_post_meta( $post->ID, self::META_PREFIX . 'artists', true );
|
||||
if ( ! is_array( $selected_artists ) ) {
|
||||
$selected_artists = array();
|
||||
}
|
||||
|
||||
$artists = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_artist',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p class="description"><?php esc_html_e( 'Select all artists featured on this track.', 'wp-fedistream' ); ?></p>
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 5px;">
|
||||
<?php foreach ( $artists as $artist ) : ?>
|
||||
<label style="display: block; padding: 2px 0;">
|
||||
<input type="checkbox" name="fedistream_track_artists[]" value="<?php echo esc_attr( $artist->ID ); ?>" <?php checked( in_array( $artist->ID, $selected_artists, true ) ); ?>>
|
||||
<?php echo esc_html( $artist->post_title ); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ( empty( $artists ) ) : ?>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: URL to add new artist */
|
||||
esc_html__( 'No artists found. %s', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( admin_url( 'post-new.php?post_type=fedistream_artist' ) ) . '">' . esc_html__( 'Add an artist first.', 'wp-fedistream' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track codes meta box.
|
||||
*
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function render_codes_meta_box( \WP_Post $post ): void {
|
||||
$isrc = get_post_meta( $post->ID, self::META_PREFIX . 'isrc', true );
|
||||
?>
|
||||
<p>
|
||||
<label for="fedistream_track_isrc"><?php esc_html_e( 'ISRC', 'wp-fedistream' ); ?></label>
|
||||
<input type="text" name="fedistream_track_isrc" id="fedistream_track_isrc" value="<?php echo esc_attr( $isrc ); ?>" class="widefat" pattern="[A-Z]{2}[A-Z0-9]{3}[0-9]{7}" title="<?php esc_attr_e( 'ISRC format: CC-XXX-YY-NNNNN', 'wp-fedistream' ); ?>">
|
||||
<span class="description"><?php esc_html_e( 'International Standard Recording Code', 'wp-fedistream' ); ?></span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save post meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function save_meta( int $post_id, \WP_Post $post ): void {
|
||||
if ( ! $this->can_save( $post_id, 'fedistream_track_save', 'fedistream_track_nonce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save audio fields.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'audio_file', 'fedistream_track_audio_file' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'duration', 'fedistream_track_duration' );
|
||||
|
||||
// Save audio format.
|
||||
if ( isset( $_POST['fedistream_track_audio_format'] ) ) {
|
||||
$allowed_formats = array( '', 'mp3', 'wav', 'flac', 'ogg' );
|
||||
$format = sanitize_text_field( wp_unslash( $_POST['fedistream_track_audio_format'] ) );
|
||||
if ( in_array( $format, $allowed_formats, true ) ) {
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'audio_format', $format );
|
||||
}
|
||||
}
|
||||
|
||||
// Save track info.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'number', 'fedistream_track_number' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'disc_number', 'fedistream_track_disc_number' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'bpm', 'fedistream_track_bpm' );
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'key', 'fedistream_track_key' );
|
||||
$this->save_bool_meta( $post_id, self::META_PREFIX . 'explicit', 'fedistream_track_explicit' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'preview_start', 'fedistream_track_preview_start' );
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'preview_duration', 'fedistream_track_preview_duration' );
|
||||
|
||||
// Save album.
|
||||
$this->save_int_meta( $post_id, self::META_PREFIX . 'album', 'fedistream_track_album' );
|
||||
|
||||
// Save artists.
|
||||
if ( isset( $_POST['fedistream_track_artists'] ) && is_array( $_POST['fedistream_track_artists'] ) ) {
|
||||
$artists = array_map( 'absint', $_POST['fedistream_track_artists'] );
|
||||
update_post_meta( $post_id, self::META_PREFIX . 'artists', $artists );
|
||||
} else {
|
||||
delete_post_meta( $post_id, self::META_PREFIX . 'artists' );
|
||||
}
|
||||
|
||||
// Save ISRC.
|
||||
$this->save_text_meta( $post_id, self::META_PREFIX . 'isrc', 'fedistream_track_isrc' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update album stats when track is saved.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
* @return void
|
||||
*/
|
||||
public function update_album_on_save( int $post_id, \WP_Post $post ): void {
|
||||
$album_id = get_post_meta( $post_id, self::META_PREFIX . 'album', true );
|
||||
if ( $album_id ) {
|
||||
Album::update_album_stats( (int) $album_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track by ID with meta.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return array|null Track data or null.
|
||||
*/
|
||||
public static function get_track( int $post_id ): ?array {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post || 'fedistream_track' !== $post->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $post_id, self::META_PREFIX . 'album', true );
|
||||
$audio_file = get_post_meta( $post_id, self::META_PREFIX . 'audio_file', true );
|
||||
$artists = get_post_meta( $post_id, self::META_PREFIX . 'artists', true ) ?: array();
|
||||
|
||||
// Get artist names.
|
||||
$artist_names = array();
|
||||
foreach ( $artists as $artist_id ) {
|
||||
$artist_names[] = get_the_title( $artist_id );
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'slug' => $post->post_name,
|
||||
'lyrics' => $post->post_content,
|
||||
'album_id' => $album_id ? (int) $album_id : null,
|
||||
'album_title' => $album_id ? get_the_title( $album_id ) : null,
|
||||
'artists' => $artists,
|
||||
'artist_names' => $artist_names,
|
||||
'track_number' => (int) get_post_meta( $post_id, self::META_PREFIX . 'number', true ),
|
||||
'disc_number' => (int) get_post_meta( $post_id, self::META_PREFIX . 'disc_number', true ) ?: 1,
|
||||
'duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'duration', true ),
|
||||
'audio_file' => $audio_file ? (int) $audio_file : null,
|
||||
'audio_url' => $audio_file ? wp_get_attachment_url( $audio_file ) : null,
|
||||
'audio_format' => get_post_meta( $post_id, self::META_PREFIX . 'audio_format', true ),
|
||||
'bpm' => (int) get_post_meta( $post_id, self::META_PREFIX . 'bpm', true ),
|
||||
'key' => get_post_meta( $post_id, self::META_PREFIX . 'key', true ),
|
||||
'explicit' => (bool) get_post_meta( $post_id, self::META_PREFIX . 'explicit', true ),
|
||||
'isrc' => get_post_meta( $post_id, self::META_PREFIX . 'isrc', true ),
|
||||
'preview_start' => (int) get_post_meta( $post_id, self::META_PREFIX . 'preview_start', true ),
|
||||
'preview_duration' => (int) get_post_meta( $post_id, self::META_PREFIX . 'preview_duration', true ) ?: 30,
|
||||
'artwork' => get_the_post_thumbnail_url( $post_id, 'large' ),
|
||||
'url' => get_permalink( $post_id ),
|
||||
);
|
||||
}
|
||||
}
|
||||
1
includes/PostTypes/index.php
Normal file
1
includes/PostTypes/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
308
includes/Roles/Capabilities.php
Normal file
308
includes/Roles/Capabilities.php
Normal file
@@ -0,0 +1,308 @@
|
||||
<?php
|
||||
/**
|
||||
* User roles and capabilities.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Roles;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Capabilities class.
|
||||
*
|
||||
* Handles custom user roles and capabilities.
|
||||
*/
|
||||
class Capabilities {
|
||||
|
||||
/**
|
||||
* Post type capabilities.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const POST_TYPE_CAPS = array(
|
||||
'fedistream_artist' => array(
|
||||
'edit_post' => 'edit_fedistream_artist',
|
||||
'read_post' => 'read_fedistream_artist',
|
||||
'delete_post' => 'delete_fedistream_artist',
|
||||
'edit_posts' => 'edit_fedistream_artists',
|
||||
'edit_others_posts' => 'edit_others_fedistream_artists',
|
||||
'publish_posts' => 'publish_fedistream_artists',
|
||||
'read_private_posts' => 'read_private_fedistream_artists',
|
||||
),
|
||||
'fedistream_album' => array(
|
||||
'edit_post' => 'edit_fedistream_album',
|
||||
'read_post' => 'read_fedistream_album',
|
||||
'delete_post' => 'delete_fedistream_album',
|
||||
'edit_posts' => 'edit_fedistream_albums',
|
||||
'edit_others_posts' => 'edit_others_fedistream_albums',
|
||||
'publish_posts' => 'publish_fedistream_albums',
|
||||
'read_private_posts' => 'read_private_fedistream_albums',
|
||||
),
|
||||
'fedistream_track' => array(
|
||||
'edit_post' => 'edit_fedistream_track',
|
||||
'read_post' => 'read_fedistream_track',
|
||||
'delete_post' => 'delete_fedistream_track',
|
||||
'edit_posts' => 'edit_fedistream_tracks',
|
||||
'edit_others_posts' => 'edit_others_fedistream_tracks',
|
||||
'publish_posts' => 'publish_fedistream_tracks',
|
||||
'read_private_posts' => 'read_private_fedistream_tracks',
|
||||
),
|
||||
'fedistream_playlist' => array(
|
||||
'edit_post' => 'edit_fedistream_playlist',
|
||||
'read_post' => 'read_fedistream_playlist',
|
||||
'delete_post' => 'delete_fedistream_playlist',
|
||||
'edit_posts' => 'edit_fedistream_playlists',
|
||||
'edit_others_posts' => 'edit_others_fedistream_playlists',
|
||||
'publish_posts' => 'publish_fedistream_playlists',
|
||||
'read_private_posts' => 'read_private_fedistream_playlists',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Taxonomy capabilities.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const TAXONOMY_CAPS = array(
|
||||
'manage_fedistream_genres',
|
||||
'manage_fedistream_moods',
|
||||
'manage_fedistream_licenses',
|
||||
);
|
||||
|
||||
/**
|
||||
* Custom capabilities.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private const CUSTOM_CAPS = array(
|
||||
'view_fedistream_stats',
|
||||
'manage_fedistream_settings',
|
||||
);
|
||||
|
||||
/**
|
||||
* Get all custom capabilities.
|
||||
*
|
||||
* @return array Array of all custom capabilities.
|
||||
*/
|
||||
public static function get_all_capabilities(): array {
|
||||
$caps = array();
|
||||
|
||||
// Post type capabilities.
|
||||
foreach ( self::POST_TYPE_CAPS as $post_type_caps ) {
|
||||
$caps = array_merge( $caps, array_values( $post_type_caps ) );
|
||||
}
|
||||
|
||||
// Delete capabilities.
|
||||
$caps[] = 'delete_fedistream_artists';
|
||||
$caps[] = 'delete_others_fedistream_artists';
|
||||
$caps[] = 'delete_published_fedistream_artists';
|
||||
$caps[] = 'delete_private_fedistream_artists';
|
||||
$caps[] = 'delete_fedistream_albums';
|
||||
$caps[] = 'delete_others_fedistream_albums';
|
||||
$caps[] = 'delete_published_fedistream_albums';
|
||||
$caps[] = 'delete_private_fedistream_albums';
|
||||
$caps[] = 'delete_fedistream_tracks';
|
||||
$caps[] = 'delete_others_fedistream_tracks';
|
||||
$caps[] = 'delete_published_fedistream_tracks';
|
||||
$caps[] = 'delete_private_fedistream_tracks';
|
||||
$caps[] = 'delete_fedistream_playlists';
|
||||
$caps[] = 'delete_others_fedistream_playlists';
|
||||
$caps[] = 'delete_published_fedistream_playlists';
|
||||
$caps[] = 'delete_private_fedistream_playlists';
|
||||
|
||||
// Taxonomy capabilities.
|
||||
$caps = array_merge( $caps, self::TAXONOMY_CAPS );
|
||||
|
||||
// Custom capabilities.
|
||||
$caps = array_merge( $caps, self::CUSTOM_CAPS );
|
||||
|
||||
return array_unique( $caps );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Artist role capabilities.
|
||||
*
|
||||
* @return array Array of capabilities for the Artist role.
|
||||
*/
|
||||
public static function get_artist_capabilities(): array {
|
||||
return array(
|
||||
// Core WordPress.
|
||||
'read' => true,
|
||||
'upload_files' => true,
|
||||
|
||||
// Own artists.
|
||||
'edit_fedistream_artists' => true,
|
||||
'edit_published_fedistream_artists' => true,
|
||||
'publish_fedistream_artists' => true,
|
||||
'delete_fedistream_artists' => true,
|
||||
'delete_published_fedistream_artists' => true,
|
||||
|
||||
// Own albums.
|
||||
'edit_fedistream_albums' => true,
|
||||
'edit_published_fedistream_albums' => true,
|
||||
'publish_fedistream_albums' => true,
|
||||
'delete_fedistream_albums' => true,
|
||||
'delete_published_fedistream_albums' => true,
|
||||
|
||||
// Own tracks.
|
||||
'edit_fedistream_tracks' => true,
|
||||
'edit_published_fedistream_tracks' => true,
|
||||
'publish_fedistream_tracks' => true,
|
||||
'delete_fedistream_tracks' => true,
|
||||
'delete_published_fedistream_tracks' => true,
|
||||
|
||||
// Own playlists.
|
||||
'edit_fedistream_playlists' => true,
|
||||
'edit_published_fedistream_playlists' => true,
|
||||
'publish_fedistream_playlists' => true,
|
||||
'delete_fedistream_playlists' => true,
|
||||
'delete_published_fedistream_playlists' => true,
|
||||
|
||||
// View stats.
|
||||
'view_fedistream_stats' => true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Label role capabilities.
|
||||
*
|
||||
* @return array Array of capabilities for the Label role.
|
||||
*/
|
||||
public static function get_label_capabilities(): array {
|
||||
$caps = self::get_artist_capabilities();
|
||||
|
||||
// Add label-specific capabilities.
|
||||
$label_caps = array(
|
||||
// Manage others' content.
|
||||
'edit_others_fedistream_artists' => true,
|
||||
'edit_others_fedistream_albums' => true,
|
||||
'edit_others_fedistream_tracks' => true,
|
||||
'edit_others_fedistream_playlists' => true,
|
||||
'delete_others_fedistream_artists' => true,
|
||||
'delete_others_fedistream_albums' => true,
|
||||
'delete_others_fedistream_tracks' => true,
|
||||
'delete_others_fedistream_playlists' => true,
|
||||
'read_private_fedistream_artists' => true,
|
||||
'read_private_fedistream_albums' => true,
|
||||
'read_private_fedistream_tracks' => true,
|
||||
'read_private_fedistream_playlists' => true,
|
||||
'delete_private_fedistream_artists' => true,
|
||||
'delete_private_fedistream_albums' => true,
|
||||
'delete_private_fedistream_tracks' => true,
|
||||
'delete_private_fedistream_playlists' => true,
|
||||
|
||||
// Manage taxonomies.
|
||||
'manage_fedistream_genres' => true,
|
||||
'manage_fedistream_moods' => true,
|
||||
'manage_fedistream_licenses' => true,
|
||||
|
||||
// View stats.
|
||||
'view_fedistream_stats' => true,
|
||||
);
|
||||
|
||||
return array_merge( $caps, $label_caps );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom roles.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_roles(): void {
|
||||
// Remove existing roles first to ensure clean slate.
|
||||
remove_role( 'fedistream_artist' );
|
||||
remove_role( 'fedistream_label' );
|
||||
|
||||
// Add Artist role.
|
||||
add_role(
|
||||
'fedistream_artist',
|
||||
__( 'FediStream Artist', 'wp-fedistream' ),
|
||||
self::get_artist_capabilities()
|
||||
);
|
||||
|
||||
// Add Label role.
|
||||
add_role(
|
||||
'fedistream_label',
|
||||
__( 'FediStream Label', 'wp-fedistream' ),
|
||||
self::get_label_capabilities()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove custom roles.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function remove_roles(): void {
|
||||
remove_role( 'fedistream_artist' );
|
||||
remove_role( 'fedistream_label' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add capabilities to administrator role.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_admin_capabilities(): void {
|
||||
$admin = get_role( 'administrator' );
|
||||
|
||||
if ( ! $admin ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add all custom capabilities.
|
||||
$all_caps = self::get_all_capabilities();
|
||||
foreach ( $all_caps as $cap ) {
|
||||
$admin->add_cap( $cap );
|
||||
}
|
||||
|
||||
// Add management capability.
|
||||
$admin->add_cap( 'manage_fedistream_settings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove capabilities from administrator role.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function remove_admin_capabilities(): void {
|
||||
$admin = get_role( 'administrator' );
|
||||
|
||||
if ( ! $admin ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all custom capabilities.
|
||||
$all_caps = self::get_all_capabilities();
|
||||
foreach ( $all_caps as $cap ) {
|
||||
$admin->remove_cap( $cap );
|
||||
}
|
||||
|
||||
$admin->remove_cap( 'manage_fedistream_settings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Install roles and capabilities.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install(): void {
|
||||
self::add_roles();
|
||||
self::add_admin_capabilities();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall roles and capabilities.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall(): void {
|
||||
self::remove_roles();
|
||||
self::remove_admin_capabilities();
|
||||
}
|
||||
}
|
||||
1
includes/Roles/index.php
Normal file
1
includes/Roles/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
67
includes/Taxonomies/AbstractTaxonomy.php
Normal file
67
includes/Taxonomies/AbstractTaxonomy.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
/**
|
||||
* Abstract base class for custom taxonomies.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Taxonomies;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract taxonomy class.
|
||||
*
|
||||
* Provides common functionality for all custom taxonomies.
|
||||
*/
|
||||
abstract class AbstractTaxonomy {
|
||||
|
||||
/**
|
||||
* Taxonomy key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $taxonomy;
|
||||
|
||||
/**
|
||||
* Post types this taxonomy applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $post_types = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'init', array( $this, 'register' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function register(): void;
|
||||
|
||||
/**
|
||||
* Get the taxonomy key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_taxonomy(): string {
|
||||
return $this->taxonomy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the post types this taxonomy is registered for.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_post_types(): array {
|
||||
return $this->post_types;
|
||||
}
|
||||
}
|
||||
154
includes/Taxonomies/Genre.php
Normal file
154
includes/Taxonomies/Genre.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
/**
|
||||
* Genre custom taxonomy.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Taxonomies;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genre taxonomy class.
|
||||
*
|
||||
* Hierarchical taxonomy for music genres.
|
||||
*/
|
||||
class Genre extends AbstractTaxonomy {
|
||||
|
||||
/**
|
||||
* Taxonomy key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $taxonomy = 'fedistream_genre';
|
||||
|
||||
/**
|
||||
* Post types this taxonomy applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $post_types = array(
|
||||
'fedistream_artist',
|
||||
'fedistream_album',
|
||||
'fedistream_track',
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Genres', 'taxonomy general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Genre', 'taxonomy singular name', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Genres', 'wp-fedistream' ),
|
||||
'popular_items' => __( 'Popular Genres', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Genres', 'wp-fedistream' ),
|
||||
'parent_item' => __( 'Parent Genre', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent Genre:', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Genre', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Genre', 'wp-fedistream' ),
|
||||
'update_item' => __( 'Update Genre', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Genre', 'wp-fedistream' ),
|
||||
'new_item_name' => __( 'New Genre Name', 'wp-fedistream' ),
|
||||
'separate_items_with_commas' => __( 'Separate genres with commas', 'wp-fedistream' ),
|
||||
'add_or_remove_items' => __( 'Add or remove genres', 'wp-fedistream' ),
|
||||
'choose_from_most_used' => __( 'Choose from the most used genres', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No genres found.', 'wp-fedistream' ),
|
||||
'no_terms' => __( 'No genres', 'wp-fedistream' ),
|
||||
'menu_name' => __( 'Genres', 'wp-fedistream' ),
|
||||
'items_list_navigation' => __( 'Genres list navigation', 'wp-fedistream' ),
|
||||
'items_list' => __( 'Genres list', 'wp-fedistream' ),
|
||||
'back_to_items' => __( '← Back to Genres', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'hierarchical' => true, // Like categories.
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'show_admin_column' => true,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_tagcloud' => true,
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'genres',
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'genre' ),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_fedistream_genres',
|
||||
'edit_terms' => 'manage_fedistream_genres',
|
||||
'delete_terms' => 'manage_fedistream_genres',
|
||||
'assign_terms' => 'edit_fedistream_tracks',
|
||||
),
|
||||
);
|
||||
|
||||
register_taxonomy( $this->taxonomy, $this->post_types, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default genres.
|
||||
*
|
||||
* @return array Array of genres with children.
|
||||
*/
|
||||
public static function get_default_genres(): array {
|
||||
return array(
|
||||
'Rock' => array( 'Alternative Rock', 'Hard Rock', 'Indie Rock', 'Progressive Rock', 'Punk Rock' ),
|
||||
'Pop' => array( 'Dance Pop', 'Electropop', 'Indie Pop', 'Synth Pop' ),
|
||||
'Electronic' => array( 'Ambient', 'Drum and Bass', 'Dubstep', 'House', 'Techno', 'Trance' ),
|
||||
'Hip Hop' => array( 'Rap', 'Trap', 'Lo-Fi Hip Hop' ),
|
||||
'Jazz' => array( 'Bebop', 'Cool Jazz', 'Free Jazz', 'Fusion' ),
|
||||
'Classical' => array( 'Baroque', 'Chamber Music', 'Opera', 'Orchestral', 'Romantic' ),
|
||||
'R&B' => array( 'Contemporary R&B', 'Neo Soul', 'Soul' ),
|
||||
'Country' => array( 'Americana', 'Bluegrass', 'Country Rock' ),
|
||||
'Metal' => array( 'Black Metal', 'Death Metal', 'Heavy Metal', 'Thrash Metal' ),
|
||||
'Folk' => array( 'Acoustic', 'Folk Rock', 'Traditional Folk' ),
|
||||
'Blues' => array( 'Chicago Blues', 'Delta Blues', 'Electric Blues' ),
|
||||
'Reggae' => array( 'Dancehall', 'Dub', 'Roots Reggae', 'Ska' ),
|
||||
'Latin' => array( 'Bossa Nova', 'Salsa', 'Reggaeton', 'Tango' ),
|
||||
'World' => array( 'African', 'Asian', 'Celtic', 'Middle Eastern' ),
|
||||
'Soundtrack' => array( 'Film Score', 'Video Game', 'Musical Theatre' ),
|
||||
'Other' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install default genres.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_defaults(): void {
|
||||
$genres = self::get_default_genres();
|
||||
|
||||
foreach ( $genres as $parent => $children ) {
|
||||
// Check if parent exists.
|
||||
$parent_term = term_exists( $parent, 'fedistream_genre' );
|
||||
|
||||
if ( ! $parent_term ) {
|
||||
$parent_term = wp_insert_term( $parent, 'fedistream_genre' );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $parent_term ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parent_id = is_array( $parent_term ) ? $parent_term['term_id'] : $parent_term;
|
||||
|
||||
// Insert children.
|
||||
foreach ( $children as $child ) {
|
||||
if ( ! term_exists( $child, 'fedistream_genre' ) ) {
|
||||
wp_insert_term(
|
||||
$child,
|
||||
'fedistream_genre',
|
||||
array( 'parent' => (int) $parent_id )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
164
includes/Taxonomies/License.php
Normal file
164
includes/Taxonomies/License.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
/**
|
||||
* License custom taxonomy.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Taxonomies;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* License taxonomy class.
|
||||
*
|
||||
* Hierarchical taxonomy for content licenses.
|
||||
*/
|
||||
class License extends AbstractTaxonomy {
|
||||
|
||||
/**
|
||||
* Taxonomy key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $taxonomy = 'fedistream_license';
|
||||
|
||||
/**
|
||||
* Post types this taxonomy applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $post_types = array(
|
||||
'fedistream_album',
|
||||
'fedistream_track',
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Licenses', 'taxonomy general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'License', 'taxonomy singular name', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Licenses', 'wp-fedistream' ),
|
||||
'popular_items' => __( 'Popular Licenses', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Licenses', 'wp-fedistream' ),
|
||||
'parent_item' => __( 'Parent License', 'wp-fedistream' ),
|
||||
'parent_item_colon' => __( 'Parent License:', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit License', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View License', 'wp-fedistream' ),
|
||||
'update_item' => __( 'Update License', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New License', 'wp-fedistream' ),
|
||||
'new_item_name' => __( 'New License Name', 'wp-fedistream' ),
|
||||
'separate_items_with_commas' => __( 'Separate licenses with commas', 'wp-fedistream' ),
|
||||
'add_or_remove_items' => __( 'Add or remove licenses', 'wp-fedistream' ),
|
||||
'choose_from_most_used' => __( 'Choose from the most used licenses', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No licenses found.', 'wp-fedistream' ),
|
||||
'no_terms' => __( 'No licenses', 'wp-fedistream' ),
|
||||
'menu_name' => __( 'Licenses', 'wp-fedistream' ),
|
||||
'items_list_navigation' => __( 'Licenses list navigation', 'wp-fedistream' ),
|
||||
'items_list' => __( 'Licenses list', 'wp-fedistream' ),
|
||||
'back_to_items' => __( '← Back to Licenses', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'hierarchical' => true, // Like categories.
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'show_admin_column' => true,
|
||||
'show_in_nav_menus' => false,
|
||||
'show_tagcloud' => false,
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'licenses',
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'license' ),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_fedistream_licenses',
|
||||
'edit_terms' => 'manage_fedistream_licenses',
|
||||
'delete_terms' => 'manage_fedistream_licenses',
|
||||
'assign_terms' => 'edit_fedistream_tracks',
|
||||
),
|
||||
);
|
||||
|
||||
register_taxonomy( $this->taxonomy, $this->post_types, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default licenses.
|
||||
*
|
||||
* @return array Array of licenses with descriptions.
|
||||
*/
|
||||
public static function get_default_licenses(): array {
|
||||
return array(
|
||||
'All Rights Reserved' => array(
|
||||
'description' => __( 'Standard copyright. All rights reserved by the creator.', 'wp-fedistream' ),
|
||||
'children' => array(),
|
||||
),
|
||||
'Creative Commons' => array(
|
||||
'description' => __( 'Creative Commons licenses for sharing and reuse.', 'wp-fedistream' ),
|
||||
'children' => array(
|
||||
'CC0' => __( 'Public Domain Dedication - No rights reserved', 'wp-fedistream' ),
|
||||
'CC BY' => __( 'Attribution - Credit must be given', 'wp-fedistream' ),
|
||||
'CC BY-SA' => __( 'Attribution-ShareAlike - Credit and share under same terms', 'wp-fedistream' ),
|
||||
'CC BY-ND' => __( 'Attribution-NoDerivs - Credit, no modifications', 'wp-fedistream' ),
|
||||
'CC BY-NC' => __( 'Attribution-NonCommercial - Credit, non-commercial only', 'wp-fedistream' ),
|
||||
'CC BY-NC-SA' => __( 'Attribution-NonCommercial-ShareAlike', 'wp-fedistream' ),
|
||||
'CC BY-NC-ND' => __( 'Attribution-NonCommercial-NoDerivs', 'wp-fedistream' ),
|
||||
),
|
||||
),
|
||||
'Public Domain' => array(
|
||||
'description' => __( 'Works in the public domain with no copyright restrictions.', 'wp-fedistream' ),
|
||||
'children' => array(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install default licenses.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_defaults(): void {
|
||||
$licenses = self::get_default_licenses();
|
||||
|
||||
foreach ( $licenses as $name => $data ) {
|
||||
// Check if parent exists.
|
||||
$parent_term = term_exists( $name, 'fedistream_license' );
|
||||
|
||||
if ( ! $parent_term ) {
|
||||
$parent_term = wp_insert_term(
|
||||
$name,
|
||||
'fedistream_license',
|
||||
array( 'description' => $data['description'] )
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_wp_error( $parent_term ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parent_id = is_array( $parent_term ) ? $parent_term['term_id'] : $parent_term;
|
||||
|
||||
// Insert children.
|
||||
foreach ( $data['children'] as $child_name => $child_desc ) {
|
||||
if ( ! term_exists( $child_name, 'fedistream_license' ) ) {
|
||||
wp_insert_term(
|
||||
$child_name,
|
||||
'fedistream_license',
|
||||
array(
|
||||
'parent' => (int) $parent_id,
|
||||
'description' => $child_desc,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
135
includes/Taxonomies/Mood.php
Normal file
135
includes/Taxonomies/Mood.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/**
|
||||
* Mood custom taxonomy.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\Taxonomies;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mood taxonomy class.
|
||||
*
|
||||
* Non-hierarchical taxonomy for track/playlist moods.
|
||||
*/
|
||||
class Mood extends AbstractTaxonomy {
|
||||
|
||||
/**
|
||||
* Taxonomy key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $taxonomy = 'fedistream_mood';
|
||||
|
||||
/**
|
||||
* Post types this taxonomy applies to.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected array $post_types = array(
|
||||
'fedistream_track',
|
||||
'fedistream_playlist',
|
||||
);
|
||||
|
||||
/**
|
||||
* Register the taxonomy.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register(): void {
|
||||
$labels = array(
|
||||
'name' => _x( 'Moods', 'taxonomy general name', 'wp-fedistream' ),
|
||||
'singular_name' => _x( 'Mood', 'taxonomy singular name', 'wp-fedistream' ),
|
||||
'search_items' => __( 'Search Moods', 'wp-fedistream' ),
|
||||
'popular_items' => __( 'Popular Moods', 'wp-fedistream' ),
|
||||
'all_items' => __( 'All Moods', 'wp-fedistream' ),
|
||||
'edit_item' => __( 'Edit Mood', 'wp-fedistream' ),
|
||||
'view_item' => __( 'View Mood', 'wp-fedistream' ),
|
||||
'update_item' => __( 'Update Mood', 'wp-fedistream' ),
|
||||
'add_new_item' => __( 'Add New Mood', 'wp-fedistream' ),
|
||||
'new_item_name' => __( 'New Mood Name', 'wp-fedistream' ),
|
||||
'separate_items_with_commas' => __( 'Separate moods with commas', 'wp-fedistream' ),
|
||||
'add_or_remove_items' => __( 'Add or remove moods', 'wp-fedistream' ),
|
||||
'choose_from_most_used' => __( 'Choose from the most used moods', 'wp-fedistream' ),
|
||||
'not_found' => __( 'No moods found.', 'wp-fedistream' ),
|
||||
'no_terms' => __( 'No moods', 'wp-fedistream' ),
|
||||
'menu_name' => __( 'Moods', 'wp-fedistream' ),
|
||||
'items_list_navigation' => __( 'Moods list navigation', 'wp-fedistream' ),
|
||||
'items_list' => __( 'Moods list', 'wp-fedistream' ),
|
||||
'back_to_items' => __( '← Back to Moods', 'wp-fedistream' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'hierarchical' => false, // Like tags.
|
||||
'public' => true,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Will be added to custom menu.
|
||||
'show_admin_column' => true,
|
||||
'show_in_nav_menus' => true,
|
||||
'show_tagcloud' => true,
|
||||
'show_in_rest' => true,
|
||||
'rest_base' => 'moods',
|
||||
'query_var' => true,
|
||||
'rewrite' => array( 'slug' => 'mood' ),
|
||||
'capabilities' => array(
|
||||
'manage_terms' => 'manage_fedistream_moods',
|
||||
'edit_terms' => 'manage_fedistream_moods',
|
||||
'delete_terms' => 'manage_fedistream_moods',
|
||||
'assign_terms' => 'edit_fedistream_tracks',
|
||||
),
|
||||
);
|
||||
|
||||
register_taxonomy( $this->taxonomy, $this->post_types, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default moods.
|
||||
*
|
||||
* @return array Array of moods.
|
||||
*/
|
||||
public static function get_default_moods(): array {
|
||||
return array(
|
||||
__( 'Energetic', 'wp-fedistream' ),
|
||||
__( 'Calm', 'wp-fedistream' ),
|
||||
__( 'Uplifting', 'wp-fedistream' ),
|
||||
__( 'Melancholic', 'wp-fedistream' ),
|
||||
__( 'Aggressive', 'wp-fedistream' ),
|
||||
__( 'Romantic', 'wp-fedistream' ),
|
||||
__( 'Happy', 'wp-fedistream' ),
|
||||
__( 'Sad', 'wp-fedistream' ),
|
||||
__( 'Relaxing', 'wp-fedistream' ),
|
||||
__( 'Intense', 'wp-fedistream' ),
|
||||
__( 'Dreamy', 'wp-fedistream' ),
|
||||
__( 'Dark', 'wp-fedistream' ),
|
||||
__( 'Groovy', 'wp-fedistream' ),
|
||||
__( 'Epic', 'wp-fedistream' ),
|
||||
__( 'Peaceful', 'wp-fedistream' ),
|
||||
__( 'Motivational', 'wp-fedistream' ),
|
||||
__( 'Nostalgic', 'wp-fedistream' ),
|
||||
__( 'Playful', 'wp-fedistream' ),
|
||||
__( 'Sensual', 'wp-fedistream' ),
|
||||
__( 'Suspenseful', 'wp-fedistream' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install default moods.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_defaults(): void {
|
||||
$moods = self::get_default_moods();
|
||||
|
||||
foreach ( $moods as $mood ) {
|
||||
if ( ! term_exists( $mood, 'fedistream_mood' ) ) {
|
||||
wp_insert_term( $mood, 'fedistream_mood' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
includes/Taxonomies/index.php
Normal file
1
includes/Taxonomies/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
794
includes/User/Library.php
Normal file
794
includes/User/Library.php
Normal file
@@ -0,0 +1,794 @@
|
||||
<?php
|
||||
/**
|
||||
* User Library class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\User;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user library features (favorites, follows, history).
|
||||
*/
|
||||
class Library {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'wp_ajax_fedistream_toggle_favorite', array( $this, 'ajax_toggle_favorite' ) );
|
||||
add_action( 'wp_ajax_fedistream_toggle_follow', array( $this, 'ajax_toggle_follow' ) );
|
||||
add_action( 'wp_ajax_fedistream_get_library', array( $this, 'ajax_get_library' ) );
|
||||
add_action( 'wp_ajax_fedistream_get_followed_artists', array( $this, 'ajax_get_followed_artists' ) );
|
||||
add_action( 'wp_ajax_fedistream_get_history', array( $this, 'ajax_get_history' ) );
|
||||
add_action( 'wp_ajax_fedistream_clear_history', array( $this, 'ajax_clear_history' ) );
|
||||
|
||||
// Add library buttons to content.
|
||||
add_filter( 'fedistream_track_actions', array( $this, 'add_favorite_button' ), 10, 2 );
|
||||
add_filter( 'fedistream_album_actions', array( $this, 'add_favorite_button' ), 10, 2 );
|
||||
add_filter( 'fedistream_artist_actions', array( $this, 'add_follow_button' ), 10, 2 );
|
||||
|
||||
// Record play history.
|
||||
add_action( 'fedistream_track_played', array( $this, 'record_play_history' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle favorite via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_toggle_favorite(): void {
|
||||
check_ajax_referer( 'fedistream_library', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$content_type = isset( $_POST['content_type'] ) ? sanitize_text_field( wp_unslash( $_POST['content_type'] ) ) : '';
|
||||
$content_id = isset( $_POST['content_id'] ) ? absint( $_POST['content_id'] ) : 0;
|
||||
|
||||
if ( ! in_array( $content_type, array( 'track', 'album', 'playlist' ), true ) || ! $content_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid request.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$is_favorited = self::is_favorited( $user_id, $content_type, $content_id );
|
||||
|
||||
if ( $is_favorited ) {
|
||||
$result = self::remove_favorite( $user_id, $content_type, $content_id );
|
||||
$action = 'removed';
|
||||
} else {
|
||||
$result = self::add_favorite( $user_id, $content_type, $content_id );
|
||||
$action = 'added';
|
||||
}
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'action' => $action,
|
||||
'is_favorited' => ! $is_favorited,
|
||||
'message' => 'added' === $action
|
||||
? __( 'Added to your library.', 'wp-fedistream' )
|
||||
: __( 'Removed from your library.', 'wp-fedistream' ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to update library.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle follow via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_toggle_follow(): void {
|
||||
check_ajax_referer( 'fedistream_library', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$artist_id = isset( $_POST['artist_id'] ) ? absint( $_POST['artist_id'] ) : 0;
|
||||
|
||||
if ( ! $artist_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid artist.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$is_following = self::is_following( $user_id, $artist_id );
|
||||
|
||||
if ( $is_following ) {
|
||||
$result = self::unfollow_artist( $user_id, $artist_id );
|
||||
$action = 'unfollowed';
|
||||
} else {
|
||||
$result = self::follow_artist( $user_id, $artist_id );
|
||||
$action = 'followed';
|
||||
}
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'action' => $action,
|
||||
'is_following' => ! $is_following,
|
||||
'message' => 'followed' === $action
|
||||
? __( 'You are now following this artist.', 'wp-fedistream' )
|
||||
: __( 'You unfollowed this artist.', 'wp-fedistream' ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to update follow status.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user library via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_get_library(): void {
|
||||
check_ajax_referer( 'fedistream_library', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$type = isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : 'all';
|
||||
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
|
||||
$per_page = 20;
|
||||
|
||||
$library = self::get_user_library( $user_id, $type, $page, $per_page );
|
||||
|
||||
wp_send_json_success( $library );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get followed artists via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_get_followed_artists(): void {
|
||||
check_ajax_referer( 'fedistream_library', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
|
||||
$per_page = 20;
|
||||
|
||||
$artists = self::get_followed_artists( $user_id, $page, $per_page );
|
||||
|
||||
wp_send_json_success( $artists );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get listening history via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_get_history(): void {
|
||||
check_ajax_referer( 'fedistream_library', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$page = isset( $_POST['page'] ) ? absint( $_POST['page'] ) : 1;
|
||||
$per_page = 50;
|
||||
|
||||
$history = self::get_listening_history( $user_id, $page, $per_page );
|
||||
|
||||
wp_send_json_success( $history );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear listening history via AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_clear_history(): void {
|
||||
check_ajax_referer( 'fedistream_library', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'You must be logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$result = self::clear_listening_history( $user_id );
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success( array( 'message' => __( 'History cleared.', 'wp-fedistream' ) ) );
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to clear history.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a favorite.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $content_type Content type (track, album, playlist).
|
||||
* @param int $content_id Content ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function add_favorite( int $user_id, string $content_type, int $content_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_favorites';
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'content_type' => $content_type,
|
||||
'content_id' => $content_id,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%d', '%s' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
do_action( 'fedistream_favorite_added', $user_id, $content_type, $content_id );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a favorite.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $content_type Content type (track, album, playlist).
|
||||
* @param int $content_id Content ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function remove_favorite( int $user_id, string $content_type, int $content_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_favorites';
|
||||
|
||||
$result = $wpdb->delete(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'content_type' => $content_type,
|
||||
'content_id' => $content_id,
|
||||
),
|
||||
array( '%d', '%s', '%d' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
do_action( 'fedistream_favorite_removed', $user_id, $content_type, $content_id );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if content is favorited.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $content_type Content type.
|
||||
* @param int $content_id Content ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_favorited( int $user_id, string $content_type, int $content_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_favorites';
|
||||
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = %s AND content_id = %d",
|
||||
$user_id,
|
||||
$content_type,
|
||||
$content_id
|
||||
)
|
||||
);
|
||||
|
||||
return (bool) $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Follow an artist.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function follow_artist( int $user_id, int $artist_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_user_follows';
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'artist_id' => $artist_id,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%d', '%s' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
do_action( 'fedistream_artist_followed', $user_id, $artist_id );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfollow an artist.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function unfollow_artist( int $user_id, int $artist_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_user_follows';
|
||||
|
||||
$result = $wpdb->delete(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'artist_id' => $artist_id,
|
||||
),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
do_action( 'fedistream_artist_unfollowed', $user_id, $artist_id );
|
||||
}
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is following an artist.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_following( int $user_id, int $artist_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_user_follows';
|
||||
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND artist_id = %d",
|
||||
$user_id,
|
||||
$artist_id
|
||||
)
|
||||
);
|
||||
|
||||
return (bool) $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record play history.
|
||||
*
|
||||
* @param int $track_id Track post ID.
|
||||
* @param int $user_id User ID (0 for anonymous).
|
||||
* @return void
|
||||
*/
|
||||
public function record_play_history( int $track_id, int $user_id ): void {
|
||||
if ( ! $user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_listening_history';
|
||||
|
||||
// Check if recently played (within last 30 seconds) to avoid duplicates.
|
||||
$recent = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND track_id = %d AND played_at > DATE_SUB(NOW(), INTERVAL 30 SECOND)",
|
||||
$user_id,
|
||||
$track_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( $recent ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'track_id' => $track_id,
|
||||
'played_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%d', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user library.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $type Content type filter (all, tracks, albums, playlists).
|
||||
* @param int $page Page number.
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_user_library( int $user_id, string $type = 'all', int $page = 1, int $per_page = 20 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_favorites';
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
$where = 'WHERE user_id = %d';
|
||||
$params = array( $user_id );
|
||||
|
||||
if ( 'all' !== $type && in_array( $type, array( 'track', 'album', 'playlist' ), true ) ) {
|
||||
$where .= ' AND content_type = %s';
|
||||
$params[] = $type;
|
||||
}
|
||||
|
||||
$total = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} {$where}",
|
||||
...$params
|
||||
)
|
||||
);
|
||||
|
||||
$params[] = $per_page;
|
||||
$params[] = $offset;
|
||||
|
||||
$favorites = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT content_type, content_id, created_at FROM {$table} {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
...$params
|
||||
)
|
||||
);
|
||||
|
||||
$items = array();
|
||||
|
||||
foreach ( $favorites as $favorite ) {
|
||||
$post = get_post( $favorite->content_id );
|
||||
|
||||
if ( ! $post || 'publish' !== $post->post_status ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = array(
|
||||
'id' => $post->ID,
|
||||
'type' => $favorite->content_type,
|
||||
'title' => $post->post_title,
|
||||
'permalink' => get_permalink( $post ),
|
||||
'added_at' => $favorite->created_at,
|
||||
'thumbnail' => get_the_post_thumbnail_url( $post, 'medium' ),
|
||||
);
|
||||
|
||||
if ( 'track' === $favorite->content_type ) {
|
||||
$item['duration'] = get_post_meta( $post->ID, '_fedistream_duration', true );
|
||||
$item['artist'] = self::get_track_artist_name( $post->ID );
|
||||
} elseif ( 'album' === $favorite->content_type ) {
|
||||
$item['artist'] = self::get_album_artist_name( $post->ID );
|
||||
$item['track_count'] = get_post_meta( $post->ID, '_fedistream_total_tracks', true );
|
||||
}
|
||||
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
return array(
|
||||
'items' => $items,
|
||||
'total' => (int) $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => (int) ceil( $total / $per_page ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get followed artists.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param int $page Page number.
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_followed_artists( int $user_id, int $page = 1, int $per_page = 20 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_user_follows';
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
$total = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
$follows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT artist_id, created_at FROM {$table} WHERE user_id = %d ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
$user_id,
|
||||
$per_page,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
$artists = array();
|
||||
|
||||
foreach ( $follows as $follow ) {
|
||||
$post = get_post( $follow->artist_id );
|
||||
|
||||
if ( ! $post || 'publish' !== $post->post_status ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$artists[] = array(
|
||||
'id' => $post->ID,
|
||||
'name' => $post->post_title,
|
||||
'permalink' => get_permalink( $post ),
|
||||
'followed_at' => $follow->created_at,
|
||||
'thumbnail' => get_the_post_thumbnail_url( $post, 'thumbnail' ),
|
||||
'type' => get_post_meta( $post->ID, '_fedistream_artist_type', true ) ?: 'solo',
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'artists' => $artists,
|
||||
'total' => (int) $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => (int) ceil( $total / $per_page ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get listening history.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param int $page Page number.
|
||||
* @param int $per_page Items per page.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_listening_history( int $user_id, int $page = 1, int $per_page = 50 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_listening_history';
|
||||
$offset = ( $page - 1 ) * $per_page;
|
||||
|
||||
$total = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
$history = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT track_id, played_at FROM {$table} WHERE user_id = %d ORDER BY played_at DESC LIMIT %d OFFSET %d",
|
||||
$user_id,
|
||||
$per_page,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
$tracks = array();
|
||||
|
||||
foreach ( $history as $item ) {
|
||||
$post = get_post( $item->track_id );
|
||||
|
||||
if ( ! $post || 'publish' !== $post->post_status ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tracks[] = array(
|
||||
'id' => $post->ID,
|
||||
'title' => $post->post_title,
|
||||
'permalink' => get_permalink( $post ),
|
||||
'played_at' => $item->played_at,
|
||||
'thumbnail' => get_the_post_thumbnail_url( $post, 'thumbnail' ),
|
||||
'duration' => get_post_meta( $post->ID, '_fedistream_duration', true ),
|
||||
'artist' => self::get_track_artist_name( $post->ID ),
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'tracks' => $tracks,
|
||||
'total' => (int) $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => (int) ceil( $total / $per_page ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear listening history.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function clear_listening_history( int $user_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_listening_history';
|
||||
$result = $wpdb->delete( $table, array( 'user_id' => $user_id ), array( '%d' ) );
|
||||
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track artist name.
|
||||
*
|
||||
* @param int $track_id Track post ID.
|
||||
* @return string
|
||||
*/
|
||||
private static function get_track_artist_name( int $track_id ): string {
|
||||
$artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true );
|
||||
|
||||
if ( is_array( $artist_ids ) && ! empty( $artist_ids ) ) {
|
||||
$names = array();
|
||||
foreach ( $artist_ids as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist ) {
|
||||
$names[] = $artist->post_title;
|
||||
}
|
||||
}
|
||||
return implode( ', ', $names );
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$artist_id = $album_id ? get_post_meta( $album_id, '_fedistream_album_artist', true ) : 0;
|
||||
|
||||
if ( $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
return $artist ? $artist->post_title : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album artist name.
|
||||
*
|
||||
* @param int $album_id Album post ID.
|
||||
* @return string
|
||||
*/
|
||||
private static function get_album_artist_name( int $album_id ): string {
|
||||
$artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true );
|
||||
|
||||
if ( $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
return $artist ? $artist->post_title : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add favorite button to track/album actions.
|
||||
*
|
||||
* @param string $actions HTML actions.
|
||||
* @param int $post_id Post ID.
|
||||
* @return string
|
||||
*/
|
||||
public function add_favorite_button( string $actions, int $post_id ): string {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$content_type = str_replace( 'fedistream_', '', $post->post_type );
|
||||
$user_id = get_current_user_id();
|
||||
$is_favorited = self::is_favorited( $user_id, $content_type, $post_id );
|
||||
|
||||
$button = sprintf(
|
||||
'<button class="fedistream-favorite-btn%s" data-content-type="%s" data-content-id="%d" title="%s">
|
||||
<span class="dashicons dashicons-heart"></span>
|
||||
</button>',
|
||||
$is_favorited ? ' is-favorited' : '',
|
||||
esc_attr( $content_type ),
|
||||
$post_id,
|
||||
$is_favorited ? esc_attr__( 'Remove from library', 'wp-fedistream' ) : esc_attr__( 'Add to library', 'wp-fedistream' )
|
||||
);
|
||||
|
||||
return $actions . $button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add follow button to artist actions.
|
||||
*
|
||||
* @param string $actions HTML actions.
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return string
|
||||
*/
|
||||
public function add_follow_button( string $actions, int $artist_id ): string {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$is_following = self::is_following( $user_id, $artist_id );
|
||||
|
||||
$button = sprintf(
|
||||
'<button class="fedistream-follow-btn%s" data-artist-id="%d">
|
||||
<span class="dashicons dashicons-%s"></span>
|
||||
<span class="button-text">%s</span>
|
||||
</button>',
|
||||
$is_following ? ' is-following' : '',
|
||||
$artist_id,
|
||||
$is_following ? 'yes' : 'plus',
|
||||
$is_following ? esc_html__( 'Following', 'wp-fedistream' ) : esc_html__( 'Follow', 'wp-fedistream' )
|
||||
);
|
||||
|
||||
return $actions . $button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's favorite count.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $content_type Optional content type filter.
|
||||
* @return int
|
||||
*/
|
||||
public static function get_favorite_count( int $user_id, string $content_type = '' ): int {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_favorites';
|
||||
|
||||
if ( $content_type ) {
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND content_type = %s",
|
||||
$user_id,
|
||||
$content_type
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's followed artist count.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return int
|
||||
*/
|
||||
public static function get_following_count( int $user_id ): int {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_user_follows';
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
}
|
||||
324
includes/User/LibraryPage.php
Normal file
324
includes/User/LibraryPage.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
/**
|
||||
* User Library Page class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\User;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the My Library page display.
|
||||
*/
|
||||
class LibraryPage {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_shortcode( 'fedistream_library', array( $this, 'render_library_shortcode' ) );
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue library scripts and styles.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_scripts(): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'fedistream-library',
|
||||
WP_FEDISTREAM_URL . 'assets/js/library.js',
|
||||
array( 'jquery' ),
|
||||
WP_FEDISTREAM_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'fedistream-library',
|
||||
'fedistreamLibrary',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'fedistream_library' ),
|
||||
'i18n' => array(
|
||||
'loading' => __( 'Loading...', 'wp-fedistream' ),
|
||||
'noFavorites' => __( 'No favorites yet.', 'wp-fedistream' ),
|
||||
'noArtists' => __( 'Not following any artists yet.', 'wp-fedistream' ),
|
||||
'noHistory' => __( 'No listening history.', 'wp-fedistream' ),
|
||||
'confirmClear' => __( 'Are you sure you want to clear your listening history?', 'wp-fedistream' ),
|
||||
'historyCleared' => __( 'History cleared.', 'wp-fedistream' ),
|
||||
'error' => __( 'An error occurred. Please try again.', 'wp-fedistream' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the library shortcode.
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string
|
||||
*/
|
||||
public function render_library_shortcode( array $atts = array() ): string {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'tab' => 'favorites',
|
||||
),
|
||||
$atts,
|
||||
'fedistream_library'
|
||||
);
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return $this->render_login_required();
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Get counts for tabs.
|
||||
$favorite_count = Library::get_favorite_count( $user_id );
|
||||
$following_count = Library::get_following_count( $user_id );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="fedistream-library" data-initial-tab="<?php echo esc_attr( $atts['tab'] ); ?>">
|
||||
<nav class="fedistream-library-nav">
|
||||
<button class="tab-btn active" data-tab="favorites">
|
||||
<?php esc_html_e( 'Favorites', 'wp-fedistream' ); ?>
|
||||
<span class="count"><?php echo esc_html( $favorite_count ); ?></span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="artists">
|
||||
<?php esc_html_e( 'Artists', 'wp-fedistream' ); ?>
|
||||
<span class="count"><?php echo esc_html( $following_count ); ?></span>
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="history">
|
||||
<?php esc_html_e( 'History', 'wp-fedistream' ); ?>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="fedistream-library-filters" data-tab="favorites">
|
||||
<select class="filter-type">
|
||||
<option value="all"><?php esc_html_e( 'All', 'wp-fedistream' ); ?></option>
|
||||
<option value="track"><?php esc_html_e( 'Tracks', 'wp-fedistream' ); ?></option>
|
||||
<option value="album"><?php esc_html_e( 'Albums', 'wp-fedistream' ); ?></option>
|
||||
<option value="playlist"><?php esc_html_e( 'Playlists', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-library-content">
|
||||
<div class="tab-content active" id="tab-favorites">
|
||||
<div class="library-grid favorites-grid">
|
||||
<!-- Loaded via AJAX -->
|
||||
</div>
|
||||
<div class="library-pagination" data-tab="favorites"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-artists">
|
||||
<div class="library-grid artists-grid">
|
||||
<!-- Loaded via AJAX -->
|
||||
</div>
|
||||
<div class="library-pagination" data-tab="artists"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="tab-history">
|
||||
<div class="history-actions">
|
||||
<button class="btn-clear-history">
|
||||
<?php esc_html_e( 'Clear History', 'wp-fedistream' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<div class="library-list history-list">
|
||||
<!-- Loaded via AJAX -->
|
||||
</div>
|
||||
<div class="library-pagination" data-tab="history"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fedistream-library-loading">
|
||||
<span class="spinner"></span>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render login required message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function render_login_required(): string {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="fedistream-login-required">
|
||||
<p><?php esc_html_e( 'Please log in to view your library.', 'wp-fedistream' ); ?></p>
|
||||
<a href="<?php echo esc_url( wp_login_url( get_permalink() ) ); ?>" class="btn btn-primary">
|
||||
<?php esc_html_e( 'Log In', 'wp-fedistream' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a favorite item.
|
||||
*
|
||||
* @param array $item Item data.
|
||||
* @return string
|
||||
*/
|
||||
public static function render_favorite_item( array $item ): string {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="library-item favorite-item" data-type="<?php echo esc_attr( $item['type'] ); ?>" data-id="<?php echo esc_attr( $item['id'] ); ?>">
|
||||
<div class="item-thumbnail">
|
||||
<?php if ( ! empty( $item['thumbnail'] ) ) : ?>
|
||||
<img src="<?php echo esc_url( $item['thumbnail'] ); ?>" alt="<?php echo esc_attr( $item['title'] ); ?>">
|
||||
<?php else : ?>
|
||||
<div class="placeholder-thumbnail">
|
||||
<span class="dashicons dashicons-<?php echo 'track' === $item['type'] ? 'format-audio' : ( 'album' === $item['type'] ? 'album' : 'playlist-audio' ); ?>"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( 'track' === $item['type'] ) : ?>
|
||||
<button class="play-btn" data-track-id="<?php echo esc_attr( $item['id'] ); ?>">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="<?php echo esc_url( $item['permalink'] ); ?>"><?php echo esc_html( $item['title'] ); ?></a>
|
||||
</h4>
|
||||
<?php if ( ! empty( $item['artist'] ) ) : ?>
|
||||
<p class="item-artist"><?php echo esc_html( $item['artist'] ); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ( 'track' === $item['type'] && ! empty( $item['duration'] ) ) : ?>
|
||||
<p class="item-duration"><?php echo esc_html( self::format_duration( $item['duration'] ) ); ?></p>
|
||||
<?php elseif ( 'album' === $item['type'] && ! empty( $item['track_count'] ) ) : ?>
|
||||
<p class="item-tracks">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: number of tracks */
|
||||
esc_html( _n( '%d track', '%d tracks', (int) $item['track_count'], 'wp-fedistream' ) ),
|
||||
(int) $item['track_count']
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="unfavorite-btn" data-content-type="<?php echo esc_attr( $item['type'] ); ?>" data-content-id="<?php echo esc_attr( $item['id'] ); ?>" title="<?php esc_attr_e( 'Remove from library', 'wp-fedistream' ); ?>">
|
||||
<span class="dashicons dashicons-heart"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an artist item.
|
||||
*
|
||||
* @param array $artist Artist data.
|
||||
* @return string
|
||||
*/
|
||||
public static function render_artist_item( array $artist ): string {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="library-item artist-item" data-id="<?php echo esc_attr( $artist['id'] ); ?>">
|
||||
<div class="item-thumbnail artist-avatar">
|
||||
<?php if ( ! empty( $artist['thumbnail'] ) ) : ?>
|
||||
<img src="<?php echo esc_url( $artist['thumbnail'] ); ?>" alt="<?php echo esc_attr( $artist['name'] ); ?>">
|
||||
<?php else : ?>
|
||||
<div class="placeholder-thumbnail">
|
||||
<span class="dashicons dashicons-<?php echo 'band' === $artist['type'] ? 'groups' : 'admin-users'; ?>"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="<?php echo esc_url( $artist['permalink'] ); ?>"><?php echo esc_html( $artist['name'] ); ?></a>
|
||||
</h4>
|
||||
<p class="item-type">
|
||||
<?php echo 'band' === $artist['type'] ? esc_html__( 'Band', 'wp-fedistream' ) : esc_html__( 'Artist', 'wp-fedistream' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button class="unfollow-btn" data-artist-id="<?php echo esc_attr( $artist['id'] ); ?>" title="<?php esc_attr_e( 'Unfollow', 'wp-fedistream' ); ?>">
|
||||
<span class="dashicons dashicons-minus"></span>
|
||||
<span class="button-text"><?php esc_html_e( 'Unfollow', 'wp-fedistream' ); ?></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a history item.
|
||||
*
|
||||
* @param array $track Track data.
|
||||
* @return string
|
||||
*/
|
||||
public static function render_history_item( array $track ): string {
|
||||
ob_start();
|
||||
?>
|
||||
<div class="library-item history-item" data-id="<?php echo esc_attr( $track['id'] ); ?>">
|
||||
<div class="item-thumbnail">
|
||||
<?php if ( ! empty( $track['thumbnail'] ) ) : ?>
|
||||
<img src="<?php echo esc_url( $track['thumbnail'] ); ?>" alt="<?php echo esc_attr( $track['title'] ); ?>">
|
||||
<?php else : ?>
|
||||
<div class="placeholder-thumbnail">
|
||||
<span class="dashicons dashicons-format-audio"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<button class="play-btn" data-track-id="<?php echo esc_attr( $track['id'] ); ?>">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="item-info">
|
||||
<h4 class="item-title">
|
||||
<a href="<?php echo esc_url( $track['permalink'] ); ?>"><?php echo esc_html( $track['title'] ); ?></a>
|
||||
</h4>
|
||||
<?php if ( ! empty( $track['artist'] ) ) : ?>
|
||||
<p class="item-artist"><?php echo esc_html( $track['artist'] ); ?></p>
|
||||
<?php endif; ?>
|
||||
<p class="item-played">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: relative time */
|
||||
esc_html__( 'Played %s', 'wp-fedistream' ),
|
||||
esc_html( human_time_diff( strtotime( $track['played_at'] ), current_time( 'timestamp' ) ) . ' ' . __( 'ago', 'wp-fedistream' ) )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="item-meta">
|
||||
<?php if ( ! empty( $track['duration'] ) ) : ?>
|
||||
<span class="item-duration"><?php echo esc_html( self::format_duration( $track['duration'] ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration in seconds to MM:SS.
|
||||
*
|
||||
* @param int $seconds Duration in seconds.
|
||||
* @return string
|
||||
*/
|
||||
private static function format_duration( int $seconds ): string {
|
||||
$minutes = floor( $seconds / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
return sprintf( '%d:%02d', $minutes, $secs );
|
||||
}
|
||||
}
|
||||
828
includes/User/Notifications.php
Normal file
828
includes/User/Notifications.php
Normal file
@@ -0,0 +1,828 @@
|
||||
<?php
|
||||
/**
|
||||
* User Notifications class.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\User;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user notifications (in-app and email).
|
||||
*/
|
||||
class Notifications {
|
||||
|
||||
/**
|
||||
* Notification types.
|
||||
*/
|
||||
const TYPE_NEW_RELEASE = 'new_release';
|
||||
const TYPE_NEW_FOLLOWER = 'new_follower';
|
||||
const TYPE_FEDIVERSE_LIKE = 'fediverse_like';
|
||||
const TYPE_FEDIVERSE_BOOST = 'fediverse_boost';
|
||||
const TYPE_PLAYLIST_ADDED = 'playlist_added';
|
||||
const TYPE_PURCHASE = 'purchase';
|
||||
const TYPE_SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// AJAX handlers.
|
||||
add_action( 'wp_ajax_fedistream_get_notifications', array( $this, 'ajax_get_notifications' ) );
|
||||
add_action( 'wp_ajax_fedistream_mark_notification_read', array( $this, 'ajax_mark_read' ) );
|
||||
add_action( 'wp_ajax_fedistream_mark_all_notifications_read', array( $this, 'ajax_mark_all_read' ) );
|
||||
add_action( 'wp_ajax_fedistream_delete_notification', array( $this, 'ajax_delete' ) );
|
||||
|
||||
// Notification triggers.
|
||||
add_action( 'fedistream_album_published', array( $this, 'notify_new_release' ), 10, 1 );
|
||||
add_action( 'fedistream_track_published', array( $this, 'notify_new_track' ), 10, 1 );
|
||||
add_action( 'fedistream_artist_followed', array( $this, 'notify_artist_followed' ), 10, 2 );
|
||||
add_action( 'fedistream_activitypub_like_received', array( $this, 'notify_fediverse_like' ), 10, 2 );
|
||||
add_action( 'fedistream_activitypub_announce_received', array( $this, 'notify_fediverse_boost' ), 10, 2 );
|
||||
|
||||
// Email notifications.
|
||||
add_action( 'fedistream_notification_created', array( $this, 'maybe_send_email' ), 10, 2 );
|
||||
|
||||
// Admin bar notification count.
|
||||
add_action( 'admin_bar_menu', array( $this, 'add_notification_indicator' ), 100 );
|
||||
|
||||
// Enqueue scripts.
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue notification scripts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_scripts(): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script(
|
||||
'fedistream-notifications',
|
||||
WP_FEDISTREAM_URL . 'assets/js/notifications.js',
|
||||
array( 'jquery' ),
|
||||
WP_FEDISTREAM_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'fedistream-notifications',
|
||||
'fedistreamNotifications',
|
||||
array(
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'fedistream_notifications' ),
|
||||
'pollInterval' => 60000, // 1 minute.
|
||||
'i18n' => array(
|
||||
'noNotifications' => __( 'No notifications', 'wp-fedistream' ),
|
||||
'markAllRead' => __( 'Mark all as read', 'wp-fedistream' ),
|
||||
'viewAll' => __( 'View all notifications', 'wp-fedistream' ),
|
||||
'justNow' => __( 'Just now', 'wp-fedistream' ),
|
||||
'error' => __( 'An error occurred.', 'wp-fedistream' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $type Notification type.
|
||||
* @param string $title Notification title.
|
||||
* @param string $message Notification message.
|
||||
* @param array $data Additional data.
|
||||
* @return int|false Notification ID or false on failure.
|
||||
*/
|
||||
public static function create( int $user_id, string $type, string $title, string $message, array $data = array() ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'data' => wp_json_encode( $data ),
|
||||
'is_read' => 0,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%s', '%s', '%s', '%d', '%s' )
|
||||
);
|
||||
|
||||
if ( $result ) {
|
||||
$notification_id = $wpdb->insert_id;
|
||||
|
||||
/**
|
||||
* Fires when a notification is created.
|
||||
*
|
||||
* @param int $notification_id Notification ID.
|
||||
* @param array $notification Notification data.
|
||||
*/
|
||||
do_action(
|
||||
'fedistream_notification_created',
|
||||
$notification_id,
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'type' => $type,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
|
||||
return $notification_id;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notifications for a user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param bool $unread_only Only get unread notifications.
|
||||
* @param int $limit Number of notifications to retrieve.
|
||||
* @param int $offset Offset for pagination.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_for_user( int $user_id, bool $unread_only = false, int $limit = 20, int $offset = 0 ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
$where = 'WHERE user_id = %d';
|
||||
$params = array( $user_id );
|
||||
|
||||
if ( $unread_only ) {
|
||||
$where .= ' AND is_read = 0';
|
||||
}
|
||||
|
||||
$params[] = $limit;
|
||||
$params[] = $offset;
|
||||
|
||||
$notifications = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} {$where} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
...$params
|
||||
)
|
||||
);
|
||||
|
||||
$result = array();
|
||||
|
||||
foreach ( $notifications as $notification ) {
|
||||
$result[] = array(
|
||||
'id' => (int) $notification->id,
|
||||
'type' => $notification->type,
|
||||
'title' => $notification->title,
|
||||
'message' => $notification->message,
|
||||
'data' => json_decode( $notification->data, true ) ?: array(),
|
||||
'is_read' => (bool) $notification->is_read,
|
||||
'created_at' => $notification->created_at,
|
||||
'read_at' => $notification->read_at,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread count for a user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return int
|
||||
*/
|
||||
public static function get_unread_count( int $user_id ): int {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$count = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND is_read = 0",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a notification as read.
|
||||
*
|
||||
* @param int $notification_id Notification ID.
|
||||
* @param int $user_id User ID (for verification).
|
||||
* @return bool
|
||||
*/
|
||||
public static function mark_read( int $notification_id, int $user_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
array(
|
||||
'is_read' => 1,
|
||||
'read_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array(
|
||||
'id' => $notification_id,
|
||||
'user_id' => $user_id,
|
||||
),
|
||||
array( '%d', '%s' ),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read for a user.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function mark_all_read( int $user_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table,
|
||||
array(
|
||||
'is_read' => 1,
|
||||
'read_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array(
|
||||
'user_id' => $user_id,
|
||||
'is_read' => 0,
|
||||
),
|
||||
array( '%d', '%s' ),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
return false !== $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a notification.
|
||||
*
|
||||
* @param int $notification_id Notification ID.
|
||||
* @param int $user_id User ID (for verification).
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete( int $notification_id, int $user_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_notifications';
|
||||
|
||||
$result = $wpdb->delete(
|
||||
$table,
|
||||
array(
|
||||
'id' => $notification_id,
|
||||
'user_id' => $user_id,
|
||||
),
|
||||
array( '%d', '%d' )
|
||||
);
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Get notifications.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_get_notifications(): void {
|
||||
check_ajax_referer( 'fedistream_notifications', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Not logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$unread_only = isset( $_POST['unread_only'] ) && $_POST['unread_only'];
|
||||
$limit = isset( $_POST['limit'] ) ? absint( $_POST['limit'] ) : 20;
|
||||
$offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;
|
||||
|
||||
$notifications = self::get_for_user( $user_id, $unread_only, $limit, $offset );
|
||||
$unread_count = self::get_unread_count( $user_id );
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'notifications' => $notifications,
|
||||
'unread_count' => $unread_count,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Mark notification as read.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_mark_read(): void {
|
||||
check_ajax_referer( 'fedistream_notifications', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Not logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$notification_id = isset( $_POST['notification_id'] ) ? absint( $_POST['notification_id'] ) : 0;
|
||||
|
||||
if ( ! $notification_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid notification.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$result = self::mark_read( $notification_id, $user_id );
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'unread_count' => self::get_unread_count( $user_id ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to update notification.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Mark all notifications as read.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_mark_all_read(): void {
|
||||
check_ajax_referer( 'fedistream_notifications', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Not logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$result = self::mark_all_read( $user_id );
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success( array( 'unread_count' => 0 ) );
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to update notifications.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: Delete notification.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function ajax_delete(): void {
|
||||
check_ajax_referer( 'fedistream_notifications', 'nonce' );
|
||||
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Not logged in.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$notification_id = isset( $_POST['notification_id'] ) ? absint( $_POST['notification_id'] ) : 0;
|
||||
|
||||
if ( ! $notification_id ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Invalid notification.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$result = self::delete( $notification_id, $user_id );
|
||||
|
||||
if ( $result ) {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'unread_count' => self::get_unread_count( $user_id ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_error( array( 'message' => __( 'Failed to delete notification.', 'wp-fedistream' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify followers of a new album release.
|
||||
*
|
||||
* @param int $album_id Album post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_new_release( int $album_id ): void {
|
||||
$album = get_post( $album_id );
|
||||
if ( ! $album ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true );
|
||||
if ( ! $artist_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all local followers of this artist.
|
||||
$followers = $this->get_artist_local_followers( $artist_id );
|
||||
|
||||
foreach ( $followers as $user_id ) {
|
||||
self::create(
|
||||
$user_id,
|
||||
self::TYPE_NEW_RELEASE,
|
||||
sprintf(
|
||||
/* translators: %s: artist name */
|
||||
__( 'New release from %s', 'wp-fedistream' ),
|
||||
$artist->post_title
|
||||
),
|
||||
sprintf(
|
||||
/* translators: 1: artist name, 2: album title */
|
||||
__( '%1$s released a new album: %2$s', 'wp-fedistream' ),
|
||||
$artist->post_title,
|
||||
$album->post_title
|
||||
),
|
||||
array(
|
||||
'album_id' => $album_id,
|
||||
'artist_id' => $artist_id,
|
||||
'album_url' => get_permalink( $album ),
|
||||
'artist_url' => get_permalink( $artist ),
|
||||
'thumbnail' => get_the_post_thumbnail_url( $album, 'thumbnail' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify followers of a new track.
|
||||
*
|
||||
* @param int $track_id Track post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_new_track( int $track_id ): void {
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get artist from track or album.
|
||||
$artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true );
|
||||
if ( empty( $artist_ids ) ) {
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$artist_id = $album_id ? get_post_meta( $album_id, '_fedistream_album_artist', true ) : 0;
|
||||
$artist_ids = $artist_id ? array( $artist_id ) : array();
|
||||
}
|
||||
|
||||
if ( empty( $artist_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_id = $artist_ids[0];
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only notify for single releases (tracks without album).
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
return; // Album release handles notification.
|
||||
}
|
||||
|
||||
$followers = $this->get_artist_local_followers( $artist_id );
|
||||
|
||||
foreach ( $followers as $user_id ) {
|
||||
self::create(
|
||||
$user_id,
|
||||
self::TYPE_NEW_RELEASE,
|
||||
sprintf(
|
||||
/* translators: %s: artist name */
|
||||
__( 'New track from %s', 'wp-fedistream' ),
|
||||
$artist->post_title
|
||||
),
|
||||
sprintf(
|
||||
/* translators: 1: artist name, 2: track title */
|
||||
__( '%1$s released a new track: %2$s', 'wp-fedistream' ),
|
||||
$artist->post_title,
|
||||
$track->post_title
|
||||
),
|
||||
array(
|
||||
'track_id' => $track_id,
|
||||
'artist_id' => $artist_id,
|
||||
'track_url' => get_permalink( $track ),
|
||||
'artist_url' => get_permalink( $artist ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify artist when they get a new local follower.
|
||||
*
|
||||
* @param int $user_id User ID who followed.
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_artist_followed( int $user_id, int $artist_id ): void {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( ! $artist ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the artist's WordPress user ID.
|
||||
$artist_user_id = get_post_meta( $artist_id, '_fedistream_user_id', true );
|
||||
if ( ! $artist_user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower = get_user_by( 'id', $user_id );
|
||||
if ( ! $follower ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::create(
|
||||
$artist_user_id,
|
||||
self::TYPE_NEW_FOLLOWER,
|
||||
__( 'New follower', 'wp-fedistream' ),
|
||||
sprintf(
|
||||
/* translators: %s: follower display name */
|
||||
__( '%s started following you', 'wp-fedistream' ),
|
||||
$follower->display_name
|
||||
),
|
||||
array(
|
||||
'follower_id' => $user_id,
|
||||
'follower_name' => $follower->display_name,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify of a Fediverse like.
|
||||
*
|
||||
* @param int $content_id Content post ID.
|
||||
* @param array $actor Actor data.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_fediverse_like( int $content_id, array $actor ): void {
|
||||
$post = get_post( $content_id );
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_user_id = $this->get_content_owner_user_id( $content_id );
|
||||
if ( ! $artist_user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor_name = $actor['name'] ?? $actor['preferredUsername'] ?? __( 'Someone', 'wp-fedistream' );
|
||||
|
||||
self::create(
|
||||
$artist_user_id,
|
||||
self::TYPE_FEDIVERSE_LIKE,
|
||||
__( 'New like from Fediverse', 'wp-fedistream' ),
|
||||
sprintf(
|
||||
/* translators: 1: actor name, 2: content title */
|
||||
__( '%1$s liked your %2$s', 'wp-fedistream' ),
|
||||
$actor_name,
|
||||
$post->post_title
|
||||
),
|
||||
array(
|
||||
'content_id' => $content_id,
|
||||
'content_type' => $post->post_type,
|
||||
'actor_uri' => $actor['id'] ?? '',
|
||||
'actor_name' => $actor_name,
|
||||
'actor_icon' => $actor['icon']['url'] ?? '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify of a Fediverse boost/announce.
|
||||
*
|
||||
* @param int $content_id Content post ID.
|
||||
* @param array $actor Actor data.
|
||||
* @return void
|
||||
*/
|
||||
public function notify_fediverse_boost( int $content_id, array $actor ): void {
|
||||
$post = get_post( $content_id );
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$artist_user_id = $this->get_content_owner_user_id( $content_id );
|
||||
if ( ! $artist_user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor_name = $actor['name'] ?? $actor['preferredUsername'] ?? __( 'Someone', 'wp-fedistream' );
|
||||
|
||||
self::create(
|
||||
$artist_user_id,
|
||||
self::TYPE_FEDIVERSE_BOOST,
|
||||
__( 'New boost from Fediverse', 'wp-fedistream' ),
|
||||
sprintf(
|
||||
/* translators: 1: actor name, 2: content title */
|
||||
__( '%1$s boosted your %2$s', 'wp-fedistream' ),
|
||||
$actor_name,
|
||||
$post->post_title
|
||||
),
|
||||
array(
|
||||
'content_id' => $content_id,
|
||||
'content_type' => $post->post_type,
|
||||
'actor_uri' => $actor['id'] ?? '',
|
||||
'actor_name' => $actor_name,
|
||||
'actor_icon' => $actor['icon']['url'] ?? '',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe send email notification.
|
||||
*
|
||||
* @param int $notification_id Notification ID.
|
||||
* @param array $notification Notification data.
|
||||
* @return void
|
||||
*/
|
||||
public function maybe_send_email( int $notification_id, array $notification ): void {
|
||||
$user_id = $notification['user_id'];
|
||||
$type = $notification['type'];
|
||||
|
||||
// Check user preference for email notifications.
|
||||
$email_enabled = get_user_meta( $user_id, 'fedistream_email_notifications', true );
|
||||
if ( '0' === $email_enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check specific notification type preference.
|
||||
$type_enabled = get_user_meta( $user_id, 'fedistream_email_' . $type, true );
|
||||
if ( '0' === $type_enabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
if ( ! $user || ! $user->user_email ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subject = sprintf(
|
||||
/* translators: 1: site name, 2: notification title */
|
||||
__( '[%1$s] %2$s', 'wp-fedistream' ),
|
||||
get_bloginfo( 'name' ),
|
||||
$notification['title']
|
||||
);
|
||||
|
||||
$message = $this->build_email_message( $notification );
|
||||
|
||||
$headers = array(
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
);
|
||||
|
||||
wp_mail( $user->user_email, $subject, $message, $headers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build email message HTML.
|
||||
*
|
||||
* @param array $notification Notification data.
|
||||
* @return string
|
||||
*/
|
||||
private function build_email_message( array $notification ): string {
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$site_url = home_url();
|
||||
|
||||
$html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>';
|
||||
$html .= '<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">';
|
||||
$html .= '<h2 style="color: #333;">' . esc_html( $notification['title'] ) . '</h2>';
|
||||
$html .= '<p style="color: #666; font-size: 16px;">' . esc_html( $notification['message'] ) . '</p>';
|
||||
|
||||
// Add action link if available.
|
||||
$data = $notification['data'];
|
||||
$link = '';
|
||||
|
||||
if ( ! empty( $data['album_url'] ) ) {
|
||||
$link = $data['album_url'];
|
||||
} elseif ( ! empty( $data['track_url'] ) ) {
|
||||
$link = $data['track_url'];
|
||||
} elseif ( ! empty( $data['artist_url'] ) ) {
|
||||
$link = $data['artist_url'];
|
||||
}
|
||||
|
||||
if ( $link ) {
|
||||
$html .= '<p><a href="' . esc_url( $link ) . '" style="display: inline-block; padding: 10px 20px; background: #0073aa; color: #fff; text-decoration: none; border-radius: 4px;">' . esc_html__( 'View Details', 'wp-fedistream' ) . '</a></p>';
|
||||
}
|
||||
|
||||
$html .= '<hr style="margin: 30px 0; border: none; border-top: 1px solid #eee;">';
|
||||
$html .= '<p style="color: #999; font-size: 12px;">' . sprintf(
|
||||
/* translators: %s: site name */
|
||||
esc_html__( 'This email was sent by %s.', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( $site_url ) . '">' . esc_html( $site_name ) . '</a>'
|
||||
) . '</p>';
|
||||
$html .= '</div></body></html>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add notification indicator to admin bar.
|
||||
*
|
||||
* @param \WP_Admin_Bar $admin_bar Admin bar instance.
|
||||
* @return void
|
||||
*/
|
||||
public function add_notification_indicator( \WP_Admin_Bar $admin_bar ): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$unread_count = self::get_unread_count( $user_id );
|
||||
|
||||
$title = '<span class="ab-icon dashicons dashicons-bell"></span>';
|
||||
if ( $unread_count > 0 ) {
|
||||
$title .= '<span class="fedistream-notification-count">' . esc_html( $unread_count ) . '</span>';
|
||||
}
|
||||
|
||||
$admin_bar->add_node(
|
||||
array(
|
||||
'id' => 'fedistream-notifications',
|
||||
'title' => $title,
|
||||
'href' => '#',
|
||||
'meta' => array(
|
||||
'class' => 'fedistream-notifications-menu',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local followers for an artist.
|
||||
*
|
||||
* @param int $artist_id Artist post ID.
|
||||
* @return array User IDs.
|
||||
*/
|
||||
private function get_artist_local_followers( int $artist_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_user_follows';
|
||||
|
||||
$user_ids = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT user_id FROM {$table} WHERE artist_id = %d",
|
||||
$artist_id
|
||||
)
|
||||
);
|
||||
|
||||
return array_map( 'intval', $user_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WordPress user ID who owns a piece of content.
|
||||
*
|
||||
* @param int $content_id Content post ID.
|
||||
* @return int|null User ID or null.
|
||||
*/
|
||||
private function get_content_owner_user_id( int $content_id ): ?int {
|
||||
$post = get_post( $content_id );
|
||||
if ( ! $post ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For tracks, get artist.
|
||||
if ( 'fedistream_track' === $post->post_type ) {
|
||||
$artist_ids = get_post_meta( $content_id, '_fedistream_artist_ids', true );
|
||||
if ( ! empty( $artist_ids ) ) {
|
||||
$artist_id = $artist_ids[0];
|
||||
return (int) get_post_meta( $artist_id, '_fedistream_user_id', true ) ?: null;
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $content_id, '_fedistream_album_id', true );
|
||||
$artist_id = $album_id ? get_post_meta( $album_id, '_fedistream_album_artist', true ) : 0;
|
||||
if ( $artist_id ) {
|
||||
return (int) get_post_meta( $artist_id, '_fedistream_user_id', true ) ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
// For albums, get artist.
|
||||
if ( 'fedistream_album' === $post->post_type ) {
|
||||
$artist_id = get_post_meta( $content_id, '_fedistream_album_artist', true );
|
||||
if ( $artist_id ) {
|
||||
return (int) get_post_meta( $artist_id, '_fedistream_user_id', true ) ?: null;
|
||||
}
|
||||
}
|
||||
|
||||
// For artists.
|
||||
if ( 'fedistream_artist' === $post->post_type ) {
|
||||
return (int) get_post_meta( $content_id, '_fedistream_user_id', true ) ?: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
499
includes/WooCommerce/AlbumProduct.php
Normal file
499
includes/WooCommerce/AlbumProduct.php
Normal file
@@ -0,0 +1,499 @@
|
||||
<?php
|
||||
/**
|
||||
* Album Product Type for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* FediStream Album product type.
|
||||
*
|
||||
* Digital product representing a FediStream album.
|
||||
*/
|
||||
class AlbumProduct extends \WC_Product {
|
||||
|
||||
/**
|
||||
* Product type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $product_type = 'fedistream_album';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int|\WC_Product|object $product Product ID or object.
|
||||
*/
|
||||
public function __construct( $product = 0 ) {
|
||||
parent::__construct( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'fedistream_album';
|
||||
}
|
||||
|
||||
/**
|
||||
* Albums are virtual products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_virtual( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Albums are downloadable products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_downloadable( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked album ID.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_linked_album_id(): int {
|
||||
return (int) $this->get_meta( '_fedistream_linked_album', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked album post.
|
||||
*
|
||||
* @return \WP_Post|null
|
||||
*/
|
||||
public function get_linked_album(): ?\WP_Post {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$album = get_post( $album_id );
|
||||
|
||||
if ( ! $album || 'fedistream_album' !== $album->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $album;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pricing type.
|
||||
*
|
||||
* @return string fixed, pwyw, or nyp
|
||||
*/
|
||||
public function get_pricing_type(): string {
|
||||
return $this->get_meta( '_fedistream_pricing_type', true ) ?: 'fixed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum price for PWYW.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_min_price(): float {
|
||||
return (float) $this->get_meta( '_fedistream_min_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested price for PWYW.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_suggested_price(): float {
|
||||
return (float) $this->get_meta( '_fedistream_suggested_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if streaming is included.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function includes_streaming(): bool {
|
||||
return 'yes' === $this->get_meta( '_fedistream_include_streaming', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available download formats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available_formats(): array {
|
||||
$formats = $this->get_meta( '_fedistream_available_formats', true );
|
||||
|
||||
return is_array( $formats ) ? $formats : array( 'mp3' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tracks in this album.
|
||||
*
|
||||
* @return array Array of WP_Post objects.
|
||||
*/
|
||||
public function get_tracks(): array {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_key' => '_fedistream_track_number', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'orderby' => 'meta_value_num',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_album_id',
|
||||
'value' => $album_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return $tracks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track count.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_track_count(): int {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$count = get_post_meta( $album_id, '_fedistream_total_tracks', true );
|
||||
|
||||
if ( $count ) {
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
return count( $this->get_tracks() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total duration in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_total_duration(): int {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$duration = get_post_meta( $album_id, '_fedistream_total_duration', true );
|
||||
|
||||
if ( $duration ) {
|
||||
return (int) $duration;
|
||||
}
|
||||
|
||||
// Calculate from tracks.
|
||||
$tracks = $this->get_tracks();
|
||||
$duration = 0;
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$duration += (int) get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
}
|
||||
|
||||
return $duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted duration.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_duration(): string {
|
||||
$seconds = $this->get_total_duration();
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hours = floor( $seconds / 3600 );
|
||||
$mins = floor( ( $seconds % 3600 ) / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
if ( $hours > 0 ) {
|
||||
return sprintf( '%d:%02d:%02d', $hours, $mins, $secs );
|
||||
}
|
||||
|
||||
return sprintf( '%d:%02d', $mins, $secs );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist name(s).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_artist_name(): string {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true );
|
||||
|
||||
if ( ! $artist_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$artist = get_post( $artist_id );
|
||||
|
||||
return $artist ? $artist->post_title : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album artwork URL.
|
||||
*
|
||||
* @param string $size Image size.
|
||||
* @return string
|
||||
*/
|
||||
public function get_album_artwork( string $size = 'medium' ): string {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$thumbnail_id = get_post_thumbnail_id( $album_id );
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_url( $thumbnail_id, $size );
|
||||
|
||||
return $image ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get release date.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_release_date(): string {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $album_id, '_fedistream_release_date', true ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album type (album, ep, single, compilation).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_album_type(): string {
|
||||
$album_id = $this->get_linked_album_id();
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $album_id, '_fedistream_album_type', true ) ?: 'album';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get downloads for this product.
|
||||
*
|
||||
* Generates downloadable files based on available formats.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return array
|
||||
*/
|
||||
public function get_downloads( $context = 'view' ): array {
|
||||
$downloads = parent::get_downloads( $context );
|
||||
|
||||
// If no manual downloads set, generate from linked album.
|
||||
if ( empty( $downloads ) && $this->get_linked_album_id() ) {
|
||||
$downloads = $this->generate_album_downloads();
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate download files from linked album.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function generate_album_downloads(): array {
|
||||
$downloads = array();
|
||||
$tracks = $this->get_tracks();
|
||||
$formats = $this->get_available_formats();
|
||||
$album = $this->get_linked_album();
|
||||
|
||||
if ( empty( $tracks ) || ! $album ) {
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
// For each format, create a download entry.
|
||||
foreach ( $formats as $format ) {
|
||||
$format_label = strtoupper( $format );
|
||||
|
||||
// Create album ZIP download entry.
|
||||
$download_id = 'album-' . $album->ID . '-' . $format;
|
||||
|
||||
$downloads[ $download_id ] = array(
|
||||
'id' => $download_id,
|
||||
'name' => sprintf(
|
||||
/* translators: 1: Album name, 2: Format name */
|
||||
__( '%1$s (%2$s)', 'wp-fedistream' ),
|
||||
$album->post_title,
|
||||
$format_label
|
||||
),
|
||||
'file' => add_query_arg(
|
||||
array(
|
||||
'fedistream_download' => 'album',
|
||||
'album_id' => $album->ID,
|
||||
'format' => $format,
|
||||
),
|
||||
home_url( '/' )
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if purchasable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable(): bool {
|
||||
// Must have a linked album.
|
||||
if ( ! $this->get_linked_album_id() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check price for fixed pricing.
|
||||
if ( 'fixed' === $this->get_pricing_type() ) {
|
||||
return $this->get_price() !== '' && $this->get_price() >= 0;
|
||||
}
|
||||
|
||||
// PWYW and NYP are always purchasable.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price HTML.
|
||||
*
|
||||
* @param string $price Price HTML.
|
||||
* @return string
|
||||
*/
|
||||
public function get_price_html( $price = '' ): string {
|
||||
$pricing_type = $this->get_pricing_type();
|
||||
|
||||
if ( 'nyp' === $pricing_type ) {
|
||||
return '<span class="fedistream-nyp-price">' . esc_html__( 'Name Your Price', 'wp-fedistream' ) . '</span>';
|
||||
}
|
||||
|
||||
if ( 'pwyw' === $pricing_type ) {
|
||||
$min_price = $this->get_min_price();
|
||||
$suggested = $this->get_suggested_price();
|
||||
|
||||
$html = '<span class="fedistream-pwyw-price">';
|
||||
|
||||
if ( $min_price > 0 ) {
|
||||
$html .= sprintf(
|
||||
/* translators: %s: Minimum price */
|
||||
esc_html__( 'From %s', 'wp-fedistream' ),
|
||||
wc_price( $min_price )
|
||||
);
|
||||
} else {
|
||||
$html .= esc_html__( 'Pay What You Want', 'wp-fedistream' );
|
||||
}
|
||||
|
||||
if ( $suggested > 0 ) {
|
||||
$html .= ' <span class="fedistream-suggested">';
|
||||
$html .= sprintf(
|
||||
/* translators: %s: Suggested price */
|
||||
esc_html__( '(Suggested: %s)', 'wp-fedistream' ),
|
||||
wc_price( $suggested )
|
||||
);
|
||||
$html .= '</span>';
|
||||
}
|
||||
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
return parent::get_price_html( $price );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to cart validation for PWYW products.
|
||||
*
|
||||
* @param bool $passed Validation passed.
|
||||
* @param int $product_id Product ID.
|
||||
* @param int $quantity Quantity.
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity ): bool {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product || 'fedistream_album' !== $product->get_type() ) {
|
||||
return $passed;
|
||||
}
|
||||
|
||||
$pricing_type = $product->get_pricing_type();
|
||||
|
||||
if ( 'pwyw' === $pricing_type || 'nyp' === $pricing_type ) {
|
||||
// Check if custom price is set.
|
||||
$custom_price = isset( $_POST['fedistream_custom_price'] ) ? // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['fedistream_custom_price'] ) ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
0;
|
||||
|
||||
$min_price = $product->get_min_price();
|
||||
|
||||
if ( 'pwyw' === $pricing_type && $custom_price < $min_price ) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
/* translators: %s: Minimum price */
|
||||
__( 'Please enter at least %s', 'wp-fedistream' ),
|
||||
wc_price( $min_price )
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store custom price in session for cart.
|
||||
WC()->session->set( 'fedistream_custom_price_' . $product_id, $custom_price );
|
||||
}
|
||||
|
||||
return $passed;
|
||||
}
|
||||
}
|
||||
474
includes/WooCommerce/DigitalDelivery.php
Normal file
474
includes/WooCommerce/DigitalDelivery.php
Normal file
@@ -0,0 +1,474 @@
|
||||
<?php
|
||||
/**
|
||||
* Digital Delivery Handler for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles digital delivery of purchased music.
|
||||
*/
|
||||
class DigitalDelivery {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Handle download requests.
|
||||
add_action( 'init', array( $this, 'handle_download_request' ) );
|
||||
|
||||
// Add download links to order emails.
|
||||
add_action( 'woocommerce_email_after_order_table', array( $this, 'add_download_links_to_email' ), 10, 4 );
|
||||
|
||||
// Add download section to My Account.
|
||||
add_action( 'woocommerce_account_downloads_endpoint', array( $this, 'customize_downloads_display' ) );
|
||||
|
||||
// Generate secure download tokens.
|
||||
add_filter( 'woocommerce_download_file_force', array( $this, 'force_download_for_audio' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle download requests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_download_request(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['fedistream_download'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = sanitize_text_field( wp_unslash( $_GET['fedistream_download'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
// Verify user is logged in.
|
||||
if ( ! is_user_logged_in() ) {
|
||||
wp_die( esc_html__( 'You must be logged in to download files.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( 'track' === $type ) {
|
||||
$this->handle_track_download( $user_id );
|
||||
} elseif ( 'album' === $type ) {
|
||||
$this->handle_album_download( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle track download.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
private function handle_track_download( int $user_id ): void {
|
||||
$track_id = isset( $_GET['track_id'] ) ? absint( $_GET['track_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$format = isset( $_GET['format'] ) ? sanitize_text_field( wp_unslash( $_GET['format'] ) ) : 'mp3'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_die( esc_html__( 'Invalid track.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Verify purchase.
|
||||
if ( ! Integration::user_has_purchased( $user_id, 'track', $track_id ) ) {
|
||||
wp_die( esc_html__( 'You have not purchased this track.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
wp_die( esc_html__( 'Track not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get audio file.
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
if ( ! $audio_id ) {
|
||||
wp_die( esc_html__( 'No audio file available.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$file_path = get_attached_file( $audio_id );
|
||||
if ( ! $file_path || ! file_exists( $file_path ) ) {
|
||||
wp_die( esc_html__( 'File not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Convert format if needed.
|
||||
$converted_file = $this->get_converted_file( $file_path, $format, $track_id );
|
||||
|
||||
// Generate filename.
|
||||
$filename = sanitize_file_name( $track->post_title ) . '.' . $format;
|
||||
|
||||
// Serve the file.
|
||||
$this->serve_file( $converted_file, $filename, $format );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle album download (ZIP of all tracks).
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return void
|
||||
*/
|
||||
private function handle_album_download( int $user_id ): void {
|
||||
$album_id = isset( $_GET['album_id'] ) ? absint( $_GET['album_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$format = isset( $_GET['format'] ) ? sanitize_text_field( wp_unslash( $_GET['format'] ) ) : 'mp3'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( ! $album_id ) {
|
||||
wp_die( esc_html__( 'Invalid album.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Verify purchase.
|
||||
if ( ! Integration::user_has_purchased( $user_id, 'album', $album_id ) ) {
|
||||
wp_die( esc_html__( 'You have not purchased this album.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$album = get_post( $album_id );
|
||||
if ( ! $album || 'fedistream_album' !== $album->post_type ) {
|
||||
wp_die( esc_html__( 'Album not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get tracks.
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_key' => '_fedistream_track_number', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'orderby' => 'meta_value_num',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_album_id',
|
||||
'value' => $album_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $tracks ) ) {
|
||||
wp_die( esc_html__( 'No tracks found in this album.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Create ZIP file.
|
||||
$zip_file = $this->create_album_zip( $album, $tracks, $format );
|
||||
|
||||
if ( ! $zip_file ) {
|
||||
wp_die( esc_html__( 'Failed to create download package.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Generate filename.
|
||||
$artist_id = get_post_meta( $album_id, '_fedistream_album_artist', true );
|
||||
$artist = $artist_id ? get_post( $artist_id ) : null;
|
||||
$artist_name = $artist ? $artist->post_title : 'Unknown Artist';
|
||||
|
||||
$filename = sanitize_file_name( $artist_name . ' - ' . $album->post_title ) . '.' . strtoupper( $format ) . '.zip';
|
||||
|
||||
// Serve the file.
|
||||
$this->serve_file( $zip_file, $filename, 'zip' );
|
||||
|
||||
// Clean up temp file.
|
||||
wp_delete_file( $zip_file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get converted file path.
|
||||
*
|
||||
* @param string $source_path Source file path.
|
||||
* @param string $format Target format.
|
||||
* @param int $track_id Track ID for caching.
|
||||
* @return string Converted file path.
|
||||
*/
|
||||
private function get_converted_file( string $source_path, string $format, int $track_id ): string {
|
||||
$source_ext = strtolower( pathinfo( $source_path, PATHINFO_EXTENSION ) );
|
||||
|
||||
// If same format, return source.
|
||||
if ( $source_ext === $format ) {
|
||||
return $source_path;
|
||||
}
|
||||
|
||||
// Check for cached conversion.
|
||||
$cache_dir = wp_upload_dir()['basedir'] . '/fedistream-cache/';
|
||||
$cache_file = $cache_dir . 'track-' . $track_id . '.' . $format;
|
||||
|
||||
if ( file_exists( $cache_file ) ) {
|
||||
return $cache_file;
|
||||
}
|
||||
|
||||
// Create cache directory.
|
||||
if ( ! file_exists( $cache_dir ) ) {
|
||||
wp_mkdir_p( $cache_dir );
|
||||
}
|
||||
|
||||
// For now, return source file (format conversion would require FFmpeg).
|
||||
// In production, you'd use FFmpeg or similar for conversion.
|
||||
// This is a placeholder for the conversion logic.
|
||||
return $source_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ZIP file for album download.
|
||||
*
|
||||
* @param \WP_Post $album Album post.
|
||||
* @param array $tracks Track posts.
|
||||
* @param string $format Audio format.
|
||||
* @return string|null ZIP file path or null on failure.
|
||||
*/
|
||||
private function create_album_zip( \WP_Post $album, array $tracks, string $format ): ?string {
|
||||
if ( ! class_exists( 'ZipArchive' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$temp_dir = get_temp_dir();
|
||||
$zip_path = $temp_dir . 'fedistream-album-' . $album->ID . '-' . time() . '.zip';
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
if ( $zip->open( $zip_path, \ZipArchive::CREATE ) !== true ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$track_number = 0;
|
||||
foreach ( $tracks as $track ) {
|
||||
++$track_number;
|
||||
|
||||
$audio_id = get_post_meta( $track->ID, '_fedistream_audio_file', true );
|
||||
if ( ! $audio_id ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_path = get_attached_file( $audio_id );
|
||||
if ( ! $file_path || ! file_exists( $file_path ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get converted file.
|
||||
$converted_file = $this->get_converted_file( $file_path, $format, $track->ID );
|
||||
|
||||
// Create filename with track number.
|
||||
$filename = sprintf(
|
||||
'%02d - %s.%s',
|
||||
$track_number,
|
||||
sanitize_file_name( $track->post_title ),
|
||||
$format
|
||||
);
|
||||
|
||||
$zip->addFile( $converted_file, $filename );
|
||||
}
|
||||
|
||||
// Add cover art if available.
|
||||
$thumbnail_id = get_post_thumbnail_id( $album->ID );
|
||||
if ( $thumbnail_id ) {
|
||||
$cover_path = get_attached_file( $thumbnail_id );
|
||||
if ( $cover_path && file_exists( $cover_path ) ) {
|
||||
$cover_ext = pathinfo( $cover_path, PATHINFO_EXTENSION );
|
||||
$zip->addFile( $cover_path, 'cover.' . $cover_ext );
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return file_exists( $zip_path ) ? $zip_path : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve a file for download.
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @param string $filename Download filename.
|
||||
* @param string $format File format.
|
||||
* @return void
|
||||
*/
|
||||
private function serve_file( string $file_path, string $filename, string $format ): void {
|
||||
if ( ! file_exists( $file_path ) ) {
|
||||
wp_die( esc_html__( 'File not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get MIME type.
|
||||
$mime_types = array(
|
||||
'mp3' => 'audio/mpeg',
|
||||
'flac' => 'audio/flac',
|
||||
'wav' => 'audio/wav',
|
||||
'ogg' => 'audio/ogg',
|
||||
'aac' => 'audio/aac',
|
||||
'zip' => 'application/zip',
|
||||
);
|
||||
|
||||
$mime_type = $mime_types[ $format ] ?? 'application/octet-stream';
|
||||
|
||||
// Clean output buffer.
|
||||
while ( ob_get_level() ) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Set headers.
|
||||
nocache_headers();
|
||||
header( 'Content-Type: ' . $mime_type );
|
||||
header( 'Content-Description: File Transfer' );
|
||||
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
|
||||
header( 'Content-Transfer-Encoding: binary' );
|
||||
header( 'Content-Length: ' . filesize( $file_path ) );
|
||||
|
||||
// Read file.
|
||||
readfile( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add download links to order confirmation email.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param bool $sent_to_admin Whether sent to admin.
|
||||
* @param bool $plain_text Whether plain text.
|
||||
* @param object $email Email object.
|
||||
* @return void
|
||||
*/
|
||||
public function add_download_links_to_email( \WC_Order $order, bool $sent_to_admin, bool $plain_text, $email ): void {
|
||||
if ( $sent_to_admin || 'completed' !== $order->get_status() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$has_fedistream = false;
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$product_type = \WC_Product_Factory::get_product_type( $item->get_product_id() );
|
||||
if ( in_array( $product_type, array( 'fedistream_album', 'fedistream_track' ), true ) ) {
|
||||
$has_fedistream = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $has_fedistream ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$downloads_url = wc_get_account_endpoint_url( 'downloads' );
|
||||
|
||||
if ( $plain_text ) {
|
||||
echo "\n\n";
|
||||
echo esc_html__( 'Your FediStream Downloads', 'wp-fedistream' ) . "\n";
|
||||
echo esc_html__( 'Access your purchased music at:', 'wp-fedistream' ) . ' ' . esc_url( $downloads_url ) . "\n";
|
||||
} else {
|
||||
?>
|
||||
<h2><?php esc_html_e( 'Your FediStream Downloads', 'wp-fedistream' ); ?></h2>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Downloads URL */
|
||||
esc_html__( 'Access your purchased music in your %s.', 'wp-fedistream' ),
|
||||
'<a href="' . esc_url( $downloads_url ) . '">' . esc_html__( 'account downloads', 'wp-fedistream' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize downloads display in My Account.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function customize_downloads_display(): void {
|
||||
if ( ! is_user_logged_in() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$purchases = $this->get_user_purchases( $user_id );
|
||||
|
||||
if ( empty( $purchases ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<h3><?php esc_html_e( 'FediStream Library', 'wp-fedistream' ); ?></h3>
|
||||
<table class="woocommerce-table shop_table shop_table_responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Title', 'wp-fedistream' ); ?></th>
|
||||
<th><?php esc_html_e( 'Type', 'wp-fedistream' ); ?></th>
|
||||
<th><?php esc_html_e( 'Purchased', 'wp-fedistream' ); ?></th>
|
||||
<th><?php esc_html_e( 'Download', 'wp-fedistream' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $purchases as $purchase ) : ?>
|
||||
<?php
|
||||
$content = get_post( $purchase->content_id );
|
||||
if ( ! $content ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$formats = array( 'mp3', 'flac' ); // Default formats.
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( $content->post_title ); ?></td>
|
||||
<td><?php echo esc_html( ucfirst( $purchase->content_type ) ); ?></td>
|
||||
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $purchase->purchased_at ) ) ); ?></td>
|
||||
<td>
|
||||
<?php foreach ( $formats as $format ) : ?>
|
||||
<?php
|
||||
$download_url = add_query_arg(
|
||||
array(
|
||||
'fedistream_download' => $purchase->content_type,
|
||||
$purchase->content_type . '_id' => $purchase->content_id,
|
||||
'format' => $format,
|
||||
),
|
||||
home_url( '/' )
|
||||
);
|
||||
?>
|
||||
<a href="<?php echo esc_url( $download_url ); ?>" class="button button-small">
|
||||
<?php echo esc_html( strtoupper( $format ) ); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's purchases.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @return array
|
||||
*/
|
||||
private function get_user_purchases( int $user_id ): array {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_purchases';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$purchases = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE user_id = %d ORDER BY purchased_at DESC",
|
||||
$user_id
|
||||
)
|
||||
);
|
||||
|
||||
return $purchases ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Force download for audio files.
|
||||
*
|
||||
* @param bool $force Force download.
|
||||
* @param string $file_path File path.
|
||||
* @return bool
|
||||
*/
|
||||
public function force_download_for_audio( bool $force, string $file_path ): bool {
|
||||
$ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );
|
||||
|
||||
$audio_extensions = array( 'mp3', 'wav', 'flac', 'ogg', 'aac', 'm4a' );
|
||||
|
||||
if ( in_array( $ext, $audio_extensions, true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $force;
|
||||
}
|
||||
}
|
||||
738
includes/WooCommerce/Integration.php
Normal file
738
includes/WooCommerce/Integration.php
Normal file
@@ -0,0 +1,738 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Integration.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main WooCommerce integration class.
|
||||
*/
|
||||
class Integration {
|
||||
|
||||
/**
|
||||
* Whether WooCommerce is active.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $woocommerce_active = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'plugins_loaded', array( $this, 'check_woocommerce' ), 5 );
|
||||
add_action( 'plugins_loaded', array( $this, 'init' ), 20 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function check_woocommerce(): void {
|
||||
$this->woocommerce_active = class_exists( 'WooCommerce' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WooCommerce integration.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init(): void {
|
||||
if ( ! $this->woocommerce_active ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register custom product types.
|
||||
add_filter( 'product_type_selector', array( $this, 'add_product_types' ) );
|
||||
add_filter( 'woocommerce_product_class', array( $this, 'product_class' ), 10, 2 );
|
||||
|
||||
// Initialize product type classes.
|
||||
add_action( 'init', array( $this, 'register_product_types' ), 5 );
|
||||
|
||||
// Add product data tabs.
|
||||
add_filter( 'woocommerce_product_data_tabs', array( $this, 'add_product_data_tabs' ) );
|
||||
add_action( 'woocommerce_product_data_panels', array( $this, 'add_product_data_panels' ) );
|
||||
|
||||
// Save product meta.
|
||||
add_action( 'woocommerce_process_product_meta', array( $this, 'save_product_meta' ) );
|
||||
|
||||
// Frontend hooks.
|
||||
add_action( 'woocommerce_single_product_summary', array( $this, 'display_track_preview' ), 25 );
|
||||
|
||||
// Purchase access hooks.
|
||||
add_action( 'woocommerce_order_status_completed', array( $this, 'grant_access_on_purchase' ) );
|
||||
|
||||
// Download hooks.
|
||||
add_filter( 'woocommerce_downloadable_file_allowed_mime_types', array( $this, 'allowed_audio_mimes' ) );
|
||||
|
||||
// Admin columns.
|
||||
add_filter( 'manage_edit-product_columns', array( $this, 'add_product_columns' ) );
|
||||
add_action( 'manage_product_posts_custom_column', array( $this, 'render_product_columns' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WooCommerce is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_active(): bool {
|
||||
return $this->woocommerce_active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register custom product types.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_product_types(): void {
|
||||
// Product types are registered via class loading.
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom product types to the selector.
|
||||
*
|
||||
* @param array $types Product types.
|
||||
* @return array Modified product types.
|
||||
*/
|
||||
public function add_product_types( array $types ): array {
|
||||
$types['fedistream_album'] = __( 'FediStream Album', 'wp-fedistream' );
|
||||
$types['fedistream_track'] = __( 'FediStream Track', 'wp-fedistream' );
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product class for custom types.
|
||||
*
|
||||
* @param string $classname Product class name.
|
||||
* @param string $product_type Product type.
|
||||
* @return string Modified class name.
|
||||
*/
|
||||
public function product_class( string $classname, string $product_type ): string {
|
||||
if ( 'fedistream_album' === $product_type ) {
|
||||
return AlbumProduct::class;
|
||||
}
|
||||
|
||||
if ( 'fedistream_track' === $product_type ) {
|
||||
return TrackProduct::class;
|
||||
}
|
||||
|
||||
return $classname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product data tabs.
|
||||
*
|
||||
* @param array $tabs Product data tabs.
|
||||
* @return array Modified tabs.
|
||||
*/
|
||||
public function add_product_data_tabs( array $tabs ): array {
|
||||
$tabs['fedistream'] = array(
|
||||
'label' => __( 'FediStream', 'wp-fedistream' ),
|
||||
'target' => 'fedistream_product_data',
|
||||
'class' => array( 'show_if_fedistream_album', 'show_if_fedistream_track' ),
|
||||
'priority' => 21,
|
||||
);
|
||||
|
||||
$tabs['fedistream_formats'] = array(
|
||||
'label' => __( 'Audio Formats', 'wp-fedistream' ),
|
||||
'target' => 'fedistream_formats_data',
|
||||
'class' => array( 'show_if_fedistream_album', 'show_if_fedistream_track' ),
|
||||
'priority' => 22,
|
||||
);
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product data panels.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_product_data_panels(): void {
|
||||
global $post;
|
||||
|
||||
$product_id = $post->ID;
|
||||
|
||||
// Get linked content.
|
||||
$linked_album = get_post_meta( $product_id, '_fedistream_linked_album', true );
|
||||
$linked_track = get_post_meta( $product_id, '_fedistream_linked_track', true );
|
||||
|
||||
// Get pricing options.
|
||||
$pricing_type = get_post_meta( $product_id, '_fedistream_pricing_type', true ) ?: 'fixed';
|
||||
$min_price = get_post_meta( $product_id, '_fedistream_min_price', true );
|
||||
$suggested_price = get_post_meta( $product_id, '_fedistream_suggested_price', true );
|
||||
|
||||
// Get format options.
|
||||
$available_formats = get_post_meta( $product_id, '_fedistream_available_formats', true ) ?: array( 'mp3' );
|
||||
$include_streaming = get_post_meta( $product_id, '_fedistream_include_streaming', true );
|
||||
|
||||
// Get albums for dropdown.
|
||||
$albums = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_album',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
// Get tracks for dropdown.
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
?>
|
||||
<div id="fedistream_product_data" class="panel woocommerce_options_panel">
|
||||
<div class="options_group show_if_fedistream_album">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_linked_album"><?php esc_html_e( 'Linked Album', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_linked_album" name="_fedistream_linked_album" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value=""><?php esc_html_e( 'Select an album...', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $albums as $album ) : ?>
|
||||
<option value="<?php echo esc_attr( $album->ID ); ?>" <?php selected( $linked_album, $album->ID ); ?>>
|
||||
<?php echo esc_html( $album->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php echo wc_help_tip( __( 'Select the FediStream album this product represents.', 'wp-fedistream' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group show_if_fedistream_track">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_linked_track"><?php esc_html_e( 'Linked Track', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_linked_track" name="_fedistream_linked_track" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value=""><?php esc_html_e( 'Select a track...', 'wp-fedistream' ); ?></option>
|
||||
<?php foreach ( $tracks as $track ) : ?>
|
||||
<option value="<?php echo esc_attr( $track->ID ); ?>" <?php selected( $linked_track, $track->ID ); ?>>
|
||||
<?php echo esc_html( $track->post_title ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php echo wc_help_tip( __( 'Select the FediStream track this product represents.', 'wp-fedistream' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group">
|
||||
<p class="form-field">
|
||||
<label for="_fedistream_pricing_type"><?php esc_html_e( 'Pricing Type', 'wp-fedistream' ); ?></label>
|
||||
<select id="_fedistream_pricing_type" name="_fedistream_pricing_type" class="wc-enhanced-select" style="width: 50%;">
|
||||
<option value="fixed" <?php selected( $pricing_type, 'fixed' ); ?>><?php esc_html_e( 'Fixed Price', 'wp-fedistream' ); ?></option>
|
||||
<option value="pwyw" <?php selected( $pricing_type, 'pwyw' ); ?>><?php esc_html_e( 'Pay What You Want', 'wp-fedistream' ); ?></option>
|
||||
<option value="nyp" <?php selected( $pricing_type, 'nyp' ); ?>><?php esc_html_e( 'Name Your Price (Free+)', 'wp-fedistream' ); ?></option>
|
||||
</select>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="options_group fedistream-pwyw-options">
|
||||
<?php
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '_fedistream_min_price',
|
||||
'label' => __( 'Minimum Price', 'wp-fedistream' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||
'desc_tip' => true,
|
||||
'description' => __( 'Minimum price for Pay What You Want. Leave empty for no minimum.', 'wp-fedistream' ),
|
||||
'type' => 'text',
|
||||
'data_type' => 'price',
|
||||
'value' => $min_price,
|
||||
)
|
||||
);
|
||||
|
||||
woocommerce_wp_text_input(
|
||||
array(
|
||||
'id' => '_fedistream_suggested_price',
|
||||
'label' => __( 'Suggested Price', 'wp-fedistream' ) . ' (' . get_woocommerce_currency_symbol() . ')',
|
||||
'desc_tip' => true,
|
||||
'description' => __( 'Suggested price shown to customers.', 'wp-fedistream' ),
|
||||
'type' => 'text',
|
||||
'data_type' => 'price',
|
||||
'value' => $suggested_price,
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="options_group">
|
||||
<?php
|
||||
woocommerce_wp_checkbox(
|
||||
array(
|
||||
'id' => '_fedistream_include_streaming',
|
||||
'label' => __( 'Include Streaming', 'wp-fedistream' ),
|
||||
'description' => __( 'Purchase unlocks full-quality streaming access.', 'wp-fedistream' ),
|
||||
'value' => $include_streaming,
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fedistream_formats_data" class="panel woocommerce_options_panel">
|
||||
<div class="options_group">
|
||||
<p class="form-field">
|
||||
<label><?php esc_html_e( 'Available Formats', 'wp-fedistream' ); ?></label>
|
||||
<span class="fedistream-format-checkboxes">
|
||||
<?php
|
||||
$formats = array(
|
||||
'mp3' => 'MP3 (320kbps)',
|
||||
'flac' => 'FLAC (Lossless)',
|
||||
'wav' => 'WAV (Uncompressed)',
|
||||
'aac' => 'AAC (256kbps)',
|
||||
'ogg' => 'OGG Vorbis',
|
||||
);
|
||||
|
||||
foreach ( $formats as $format => $label ) :
|
||||
$checked = is_array( $available_formats ) && in_array( $format, $available_formats, true );
|
||||
?>
|
||||
<label style="display: block; margin-bottom: 5px;">
|
||||
<input type="checkbox" name="_fedistream_available_formats[]" value="<?php echo esc_attr( $format ); ?>" <?php checked( $checked ); ?>>
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
</p>
|
||||
<p class="description" style="margin-left: 150px;">
|
||||
<?php esc_html_e( 'Select which audio formats customers can download after purchase.', 'wp-fedistream' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
jQuery(function($) {
|
||||
function togglePricingOptions() {
|
||||
var type = $('#_fedistream_pricing_type').val();
|
||||
if (type === 'pwyw' || type === 'nyp') {
|
||||
$('.fedistream-pwyw-options').show();
|
||||
} else {
|
||||
$('.fedistream-pwyw-options').hide();
|
||||
}
|
||||
}
|
||||
|
||||
$('#_fedistream_pricing_type').on('change', togglePricingOptions);
|
||||
togglePricingOptions();
|
||||
|
||||
// Show/hide tabs based on product type.
|
||||
$('input#_virtual, input#_downloadable').on('change', function() {
|
||||
var type = $('select#product-type').val();
|
||||
if (type === 'fedistream_album' || type === 'fedistream_track') {
|
||||
$('input#_virtual').prop('checked', true);
|
||||
$('input#_downloadable').prop('checked', true);
|
||||
}
|
||||
});
|
||||
|
||||
$('select#product-type').on('change', function() {
|
||||
var type = $(this).val();
|
||||
if (type === 'fedistream_album' || type === 'fedistream_track') {
|
||||
$('input#_virtual').prop('checked', true).trigger('change');
|
||||
$('input#_downloadable').prop('checked', true).trigger('change');
|
||||
}
|
||||
}).trigger('change');
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Save product meta.
|
||||
*
|
||||
* @param int $product_id Product ID.
|
||||
* @return void
|
||||
*/
|
||||
public function save_product_meta( int $product_id ): void {
|
||||
// Linked content.
|
||||
if ( isset( $_POST['_fedistream_linked_album'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_linked_album', absint( $_POST['_fedistream_linked_album'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_linked_track'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_linked_track', absint( $_POST['_fedistream_linked_track'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
// Pricing options.
|
||||
if ( isset( $_POST['_fedistream_pricing_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_pricing_type', sanitize_text_field( wp_unslash( $_POST['_fedistream_pricing_type'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_min_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_min_price', wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['_fedistream_min_price'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
if ( isset( $_POST['_fedistream_suggested_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_suggested_price', wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['_fedistream_suggested_price'] ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
|
||||
// Streaming access.
|
||||
$include_streaming = isset( $_POST['_fedistream_include_streaming'] ) ? 'yes' : 'no'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_include_streaming', $include_streaming );
|
||||
|
||||
// Available formats.
|
||||
if ( isset( $_POST['_fedistream_available_formats'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
$formats = array_map( 'sanitize_text_field', wp_unslash( $_POST['_fedistream_available_formats'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
update_post_meta( $product_id, '_fedistream_available_formats', $formats );
|
||||
} else {
|
||||
update_post_meta( $product_id, '_fedistream_available_formats', array() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display track preview on product page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function display_track_preview(): void {
|
||||
global $product;
|
||||
|
||||
if ( ! $product ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product_type = $product->get_type();
|
||||
|
||||
if ( 'fedistream_track' === $product_type ) {
|
||||
$track_id = get_post_meta( $product->get_id(), '_fedistream_linked_track', true );
|
||||
if ( $track_id ) {
|
||||
$this->render_track_preview( $track_id );
|
||||
}
|
||||
} elseif ( 'fedistream_album' === $product_type ) {
|
||||
$album_id = get_post_meta( $product->get_id(), '_fedistream_linked_album', true );
|
||||
if ( $album_id ) {
|
||||
$this->render_album_preview( $album_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render track preview player.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return void
|
||||
*/
|
||||
private function render_track_preview( int $track_id ): void {
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
$audio_url = $audio_id ? wp_get_attachment_url( $audio_id ) : '';
|
||||
|
||||
if ( ! $audio_url ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$duration = get_post_meta( $track_id, '_fedistream_duration', true );
|
||||
|
||||
?>
|
||||
<div class="fedistream-product-preview">
|
||||
<h4><?php esc_html_e( 'Preview', 'wp-fedistream' ); ?></h4>
|
||||
<div class="fedistream-mini-player" data-track-id="<?php echo esc_attr( $track_id ); ?>">
|
||||
<button class="fedistream-preview-play" type="button" aria-label="<?php esc_attr_e( 'Play preview', 'wp-fedistream' ); ?>">
|
||||
<span class="dashicons dashicons-controls-play"></span>
|
||||
</button>
|
||||
<div class="fedistream-preview-progress">
|
||||
<div class="fedistream-preview-progress-bar"></div>
|
||||
</div>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="fedistream-preview-duration"><?php echo esc_html( gmdate( 'i:s', $duration ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render album preview tracklist.
|
||||
*
|
||||
* @param int $album_id Album ID.
|
||||
* @return void
|
||||
*/
|
||||
private function render_album_preview( int $album_id ): void {
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_key' => '_fedistream_track_number', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'orderby' => 'meta_value_num',
|
||||
'order' => 'ASC',
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_album_id',
|
||||
'value' => $album_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $tracks ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
?>
|
||||
<div class="fedistream-product-tracklist">
|
||||
<h4><?php esc_html_e( 'Tracklist', 'wp-fedistream' ); ?></h4>
|
||||
<ol class="fedistream-album-tracks">
|
||||
<?php foreach ( $tracks as $track ) : ?>
|
||||
<?php
|
||||
$duration = get_post_meta( $track->ID, '_fedistream_duration', true );
|
||||
?>
|
||||
<li class="fedistream-album-track" data-track-id="<?php echo esc_attr( $track->ID ); ?>">
|
||||
<span class="fedistream-track-title"><?php echo esc_html( $track->post_title ); ?></span>
|
||||
<?php if ( $duration ) : ?>
|
||||
<span class="fedistream-track-duration"><?php echo esc_html( gmdate( 'i:s', $duration ) ); ?></span>
|
||||
<?php endif; ?>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ol>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant streaming/download access on purchase completion.
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
public function grant_access_on_purchase( int $order_id ): void {
|
||||
$order = wc_get_order( $order_id );
|
||||
|
||||
if ( ! $order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$customer_id = $order->get_customer_id();
|
||||
if ( ! $customer_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $order->get_items() as $item ) {
|
||||
$product_id = $item->get_product_id();
|
||||
$product_type = \WC_Product_Factory::get_product_type( $product_id );
|
||||
|
||||
if ( 'fedistream_album' === $product_type ) {
|
||||
$album_id = get_post_meta( $product_id, '_fedistream_linked_album', true );
|
||||
if ( $album_id ) {
|
||||
$this->grant_album_access( $customer_id, $album_id, $order_id );
|
||||
}
|
||||
} elseif ( 'fedistream_track' === $product_type ) {
|
||||
$track_id = get_post_meta( $product_id, '_fedistream_linked_track', true );
|
||||
if ( $track_id ) {
|
||||
$this->grant_track_access( $customer_id, $track_id, $order_id );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant album access to a customer.
|
||||
*
|
||||
* @param int $customer_id Customer ID.
|
||||
* @param int $album_id Album ID.
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
private function grant_album_access( int $customer_id, int $album_id, int $order_id ): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_purchases';
|
||||
|
||||
// Check if access already exists.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = 'album' AND content_id = %d",
|
||||
$customer_id,
|
||||
$album_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( $exists ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $customer_id,
|
||||
'content_type' => 'album',
|
||||
'content_id' => $album_id,
|
||||
'order_id' => $order_id,
|
||||
'purchased_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%d', '%d', '%s' )
|
||||
);
|
||||
|
||||
// Also grant access to all tracks in the album.
|
||||
$tracks = get_posts(
|
||||
array(
|
||||
'post_type' => 'fedistream_track',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_album_id',
|
||||
'value' => $album_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $tracks as $track ) {
|
||||
$this->grant_track_access( $customer_id, $track->ID, $order_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant track access to a customer.
|
||||
*
|
||||
* @param int $customer_id Customer ID.
|
||||
* @param int $track_id Track ID.
|
||||
* @param int $order_id Order ID.
|
||||
* @return void
|
||||
*/
|
||||
private function grant_track_access( int $customer_id, int $track_id, int $order_id ): void {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_purchases';
|
||||
|
||||
// Check if access already exists.
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = 'track' AND content_id = %d",
|
||||
$customer_id,
|
||||
$track_id
|
||||
)
|
||||
);
|
||||
|
||||
if ( $exists ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->insert(
|
||||
$table,
|
||||
array(
|
||||
'user_id' => $customer_id,
|
||||
'content_type' => 'track',
|
||||
'content_id' => $track_id,
|
||||
'order_id' => $order_id,
|
||||
'purchased_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%d', '%d', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has purchased content.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param string $content_type Content type (album or track).
|
||||
* @param int $content_id Content ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function user_has_purchased( int $user_id, string $content_type, int $content_id ): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'fedistream_purchases';
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT id FROM {$table} WHERE user_id = %d AND content_type = %s AND content_id = %d",
|
||||
$user_id,
|
||||
$content_type,
|
||||
$content_id
|
||||
)
|
||||
);
|
||||
|
||||
return (bool) $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add allowed audio MIME types.
|
||||
*
|
||||
* @param array $types Allowed MIME types.
|
||||
* @return array Modified MIME types.
|
||||
*/
|
||||
public function allowed_audio_mimes( array $types ): array {
|
||||
$types['flac'] = 'audio/flac';
|
||||
$types['wav'] = 'audio/wav';
|
||||
$types['ogg'] = 'audio/ogg';
|
||||
$types['aac'] = 'audio/aac';
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add product columns.
|
||||
*
|
||||
* @param array $columns Columns.
|
||||
* @return array Modified columns.
|
||||
*/
|
||||
public function add_product_columns( array $columns ): array {
|
||||
$new_columns = array();
|
||||
|
||||
foreach ( $columns as $key => $value ) {
|
||||
$new_columns[ $key ] = $value;
|
||||
|
||||
if ( 'product_type' === $key ) {
|
||||
$new_columns['fedistream_linked'] = __( 'FediStream', 'wp-fedistream' );
|
||||
}
|
||||
}
|
||||
|
||||
return $new_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render product columns.
|
||||
*
|
||||
* @param string $column Column name.
|
||||
* @param int $post_id Post ID.
|
||||
* @return void
|
||||
*/
|
||||
public function render_product_columns( string $column, int $post_id ): void {
|
||||
if ( 'fedistream_linked' !== $column ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$product_type = \WC_Product_Factory::get_product_type( $post_id );
|
||||
|
||||
if ( 'fedistream_album' === $product_type ) {
|
||||
$album_id = get_post_meta( $post_id, '_fedistream_linked_album', true );
|
||||
if ( $album_id ) {
|
||||
$album = get_post( $album_id );
|
||||
if ( $album ) {
|
||||
echo '<a href="' . esc_url( get_edit_post_link( $album_id ) ) . '">' . esc_html( $album->post_title ) . '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
} elseif ( 'fedistream_track' === $product_type ) {
|
||||
$track_id = get_post_meta( $post_id, '_fedistream_linked_track', true );
|
||||
if ( $track_id ) {
|
||||
$track = get_post( $track_id );
|
||||
if ( $track ) {
|
||||
echo '<a href="' . esc_url( get_edit_post_link( $track_id ) ) . '">' . esc_html( $track->post_title ) . '</a>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
} else {
|
||||
echo '<span class="na">–</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
416
includes/WooCommerce/StreamingAccess.php
Normal file
416
includes/WooCommerce/StreamingAccess.php
Normal file
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
/**
|
||||
* Streaming Access Control for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls streaming access based on purchases.
|
||||
*/
|
||||
class StreamingAccess {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Filter audio URL access.
|
||||
add_filter( 'fedistream_can_stream_track', array( $this, 'can_stream_track' ), 10, 3 );
|
||||
|
||||
// Add streaming access check to AJAX handler.
|
||||
add_filter( 'fedistream_track_data', array( $this, 'filter_track_data' ), 10, 2 );
|
||||
|
||||
// Handle preview access.
|
||||
add_action( 'init', array( $this, 'handle_preview_request' ) );
|
||||
|
||||
// Add purchase buttons to track display.
|
||||
add_action( 'fedistream_after_track_player', array( $this, 'add_purchase_button' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user can stream a track.
|
||||
*
|
||||
* @param bool $can_stream Default access (true).
|
||||
* @param int $track_id Track ID.
|
||||
* @param int $user_id User ID (0 for guests).
|
||||
* @return bool
|
||||
*/
|
||||
public function can_stream_track( bool $can_stream, int $track_id, int $user_id ): bool {
|
||||
// Check if WooCommerce integration is enabled.
|
||||
if ( ! get_option( 'wp_fedistream_enable_woocommerce', 0 ) ) {
|
||||
return $can_stream;
|
||||
}
|
||||
|
||||
// Check if track requires purchase.
|
||||
$requires_purchase = $this->track_requires_purchase( $track_id );
|
||||
|
||||
if ( ! $requires_purchase ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Guest users can't stream paid content.
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user has purchased this track.
|
||||
if ( Integration::user_has_purchased( $user_id, 'track', $track_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has purchased the album containing this track.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id && Integration::user_has_purchased( $user_id, 'album', $album_id ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a track requires purchase to stream.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return bool
|
||||
*/
|
||||
private function track_requires_purchase( int $track_id ): bool {
|
||||
// Find WooCommerce products linked to this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( empty( $products ) ) {
|
||||
// Also check album products.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$products = $this->get_products_for_album( $album_id );
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $products ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if any product includes streaming.
|
||||
foreach ( $products as $product ) {
|
||||
$include_streaming = get_post_meta( $product->ID, '_fedistream_include_streaming', true );
|
||||
if ( 'yes' === $include_streaming ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WooCommerce products linked to a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return array
|
||||
*/
|
||||
private function get_products_for_track( int $track_id ): array {
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_linked_track',
|
||||
'value' => $track_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WooCommerce products linked to an album.
|
||||
*
|
||||
* @param int $album_id Album ID.
|
||||
* @return array
|
||||
*/
|
||||
private function get_products_for_album( int $album_id ): array {
|
||||
return get_posts(
|
||||
array(
|
||||
'post_type' => 'product',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
array(
|
||||
'key' => '_fedistream_linked_album',
|
||||
'value' => $album_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter track data for AJAX responses.
|
||||
*
|
||||
* @param array $data Track data.
|
||||
* @param int $track_id Track ID.
|
||||
* @return array Modified track data.
|
||||
*/
|
||||
public function filter_track_data( array $data, int $track_id ): array {
|
||||
$user_id = get_current_user_id();
|
||||
$can_stream = apply_filters( 'fedistream_can_stream_track', true, $track_id, $user_id );
|
||||
|
||||
if ( ! $can_stream ) {
|
||||
// Return preview URL instead of full audio.
|
||||
$data['audio_url'] = $this->get_preview_url( $track_id );
|
||||
$data['preview_only'] = true;
|
||||
$data['purchase_url'] = $this->get_purchase_url( $track_id );
|
||||
} else {
|
||||
$data['preview_only'] = false;
|
||||
}
|
||||
|
||||
// Add purchase status.
|
||||
$data['user_has_purchased'] = $user_id && (
|
||||
Integration::user_has_purchased( $user_id, 'track', $track_id ) ||
|
||||
Integration::user_has_purchased( $user_id, 'album', get_post_meta( $track_id, '_fedistream_album_id', true ) )
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview URL for a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return string
|
||||
*/
|
||||
private function get_preview_url( int $track_id ): string {
|
||||
return add_query_arg(
|
||||
array(
|
||||
'fedistream_preview' => $track_id,
|
||||
'_' => time(), // Cache buster.
|
||||
),
|
||||
home_url( '/' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get purchase URL for a track.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return string
|
||||
*/
|
||||
private function get_purchase_url( int $track_id ): string {
|
||||
// Find product for this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( ! empty( $products ) ) {
|
||||
return get_permalink( $products[0]->ID );
|
||||
}
|
||||
|
||||
// Check for album product.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$album_products = $this->get_products_for_album( $album_id );
|
||||
if ( ! empty( $album_products ) ) {
|
||||
return get_permalink( $album_products[0]->ID );
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle preview request.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle_preview_request(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['fedistream_preview'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$track_id = absint( $_GET['fedistream_preview'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( ! $track_id ) {
|
||||
wp_die( esc_html__( 'Invalid track.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
wp_die( esc_html__( 'Track not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get audio file.
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
if ( ! $audio_id ) {
|
||||
wp_die( esc_html__( 'No audio file available.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
$file_path = get_attached_file( $audio_id );
|
||||
if ( ! $file_path || ! file_exists( $file_path ) ) {
|
||||
wp_die( esc_html__( 'File not found.', 'wp-fedistream' ) );
|
||||
}
|
||||
|
||||
// Get preview settings.
|
||||
$preview_start = (int) get_post_meta( $track_id, '_fedistream_preview_start', true );
|
||||
$preview_duration = (int) get_post_meta( $track_id, '_fedistream_preview_duration', true );
|
||||
|
||||
// Default 30 seconds preview.
|
||||
if ( ! $preview_duration ) {
|
||||
$preview_duration = 30;
|
||||
}
|
||||
|
||||
// For now, serve the full file with range headers.
|
||||
// In production, you'd use FFmpeg to extract a preview clip.
|
||||
$this->serve_audio_preview( $file_path, $preview_start, $preview_duration );
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve audio preview with limited duration.
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @param int $start_seconds Start time in seconds.
|
||||
* @param int $duration_seconds Duration in seconds.
|
||||
* @return void
|
||||
*/
|
||||
private function serve_audio_preview( string $file_path, int $start_seconds, int $duration_seconds ): void {
|
||||
$file_size = filesize( $file_path );
|
||||
$mime_type = wp_check_filetype( $file_path )['type'] ?: 'audio/mpeg';
|
||||
|
||||
// Calculate byte range for preview (rough approximation).
|
||||
// This is a simplified approach; proper implementation would use FFmpeg.
|
||||
$duration_total = (int) get_post_meta( $this->get_track_id_from_path( $file_path ), '_fedistream_duration', true );
|
||||
|
||||
if ( $duration_total > 0 ) {
|
||||
$bytes_per_second = $file_size / $duration_total;
|
||||
$start_byte = (int) ( $start_seconds * $bytes_per_second );
|
||||
$end_byte = (int) min( ( $start_seconds + $duration_seconds ) * $bytes_per_second, $file_size - 1 );
|
||||
} else {
|
||||
// Serve first 30% of file as fallback.
|
||||
$start_byte = 0;
|
||||
$end_byte = (int) ( $file_size * 0.3 );
|
||||
}
|
||||
|
||||
// Clean output buffer.
|
||||
while ( ob_get_level() ) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Set headers for partial content.
|
||||
header( 'HTTP/1.1 206 Partial Content' );
|
||||
header( 'Content-Type: ' . $mime_type );
|
||||
header( 'Accept-Ranges: bytes' );
|
||||
header( 'Content-Length: ' . ( $end_byte - $start_byte + 1 ) );
|
||||
header( "Content-Range: bytes {$start_byte}-{$end_byte}/{$file_size}" );
|
||||
header( 'Content-Disposition: inline' );
|
||||
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
||||
|
||||
// Serve partial content.
|
||||
$fp = fopen( $file_path, 'rb' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
|
||||
if ( $fp ) {
|
||||
fseek( $fp, $start_byte );
|
||||
echo fread( $fp, $end_byte - $start_byte + 1 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread,WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
fclose( $fp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track ID from file path (for cached lookups).
|
||||
*
|
||||
* @param string $file_path File path.
|
||||
* @return int Track ID or 0.
|
||||
*/
|
||||
private function get_track_id_from_path( string $file_path ): int {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$attachment_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM {$wpdb->posts} WHERE guid LIKE %s",
|
||||
'%' . $wpdb->esc_like( basename( $file_path ) )
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $attachment_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$track_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_fedistream_audio_file' AND meta_value = %d",
|
||||
$attachment_id
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $track_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add purchase button after track player.
|
||||
*
|
||||
* @param int $track_id Track ID.
|
||||
* @return void
|
||||
*/
|
||||
public function add_purchase_button( int $track_id ): void {
|
||||
if ( ! get_option( 'wp_fedistream_enable_woocommerce', 0 ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
// Check if already purchased.
|
||||
if ( $user_id ) {
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
|
||||
if ( Integration::user_has_purchased( $user_id, 'track', $track_id ) ) {
|
||||
echo '<p class="fedistream-purchase-status">' . esc_html__( 'You own this track.', 'wp-fedistream' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $album_id && Integration::user_has_purchased( $user_id, 'album', $album_id ) ) {
|
||||
echo '<p class="fedistream-purchase-status">' . esc_html__( 'You own this album.', 'wp-fedistream' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find product for this track.
|
||||
$products = $this->get_products_for_track( $track_id );
|
||||
|
||||
if ( ! empty( $products ) ) {
|
||||
$product = wc_get_product( $products[0]->ID );
|
||||
if ( $product ) {
|
||||
echo '<div class="fedistream-purchase-button">';
|
||||
echo '<a href="' . esc_url( get_permalink( $products[0]->ID ) ) . '" class="button">';
|
||||
echo esc_html__( 'Buy Track', 'wp-fedistream' ) . ' - ' . wp_kses_post( $product->get_price_html() );
|
||||
echo '</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Also show album option if available.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
if ( $album_id ) {
|
||||
$album_products = $this->get_products_for_album( $album_id );
|
||||
if ( ! empty( $album_products ) ) {
|
||||
$album_product = wc_get_product( $album_products[0]->ID );
|
||||
$album = get_post( $album_id );
|
||||
if ( $album_product && $album ) {
|
||||
echo '<div class="fedistream-purchase-button fedistream-purchase-album">';
|
||||
echo '<a href="' . esc_url( get_permalink( $album_products[0]->ID ) ) . '" class="button button-secondary">';
|
||||
/* translators: %s: Album name */
|
||||
echo esc_html( sprintf( __( 'Buy Full Album: %s', 'wp-fedistream' ), $album->post_title ) );
|
||||
echo ' - ' . wp_kses_post( $album_product->get_price_html() );
|
||||
echo '</a>';
|
||||
echo '</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
520
includes/WooCommerce/TrackProduct.php
Normal file
520
includes/WooCommerce/TrackProduct.php
Normal file
@@ -0,0 +1,520 @@
|
||||
<?php
|
||||
/**
|
||||
* Track Product Type for WooCommerce.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
namespace WP_FediStream\WooCommerce;
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* FediStream Track product type.
|
||||
*
|
||||
* Digital product representing a single FediStream track.
|
||||
*/
|
||||
class TrackProduct extends \WC_Product {
|
||||
|
||||
/**
|
||||
* Product type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $product_type = 'fedistream_track';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int|\WC_Product|object $product Product ID or object.
|
||||
*/
|
||||
public function __construct( $product = 0 ) {
|
||||
parent::__construct( $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get product type.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_type(): string {
|
||||
return 'fedistream_track';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks are virtual products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_virtual( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tracks are downloadable products.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return bool
|
||||
*/
|
||||
public function get_downloadable( $context = 'view' ): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked track ID.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_linked_track_id(): int {
|
||||
return (int) $this->get_meta( '_fedistream_linked_track', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the linked track post.
|
||||
*
|
||||
* @return \WP_Post|null
|
||||
*/
|
||||
public function get_linked_track(): ?\WP_Post {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$track = get_post( $track_id );
|
||||
|
||||
if ( ! $track || 'fedistream_track' !== $track->post_type ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the pricing type.
|
||||
*
|
||||
* @return string fixed, pwyw, or nyp
|
||||
*/
|
||||
public function get_pricing_type(): string {
|
||||
return $this->get_meta( '_fedistream_pricing_type', true ) ?: 'fixed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimum price for PWYW.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_min_price(): float {
|
||||
return (float) $this->get_meta( '_fedistream_min_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get suggested price for PWYW.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function get_suggested_price(): float {
|
||||
return (float) $this->get_meta( '_fedistream_suggested_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if streaming is included.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function includes_streaming(): bool {
|
||||
return 'yes' === $this->get_meta( '_fedistream_include_streaming', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available download formats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available_formats(): array {
|
||||
$formats = $this->get_meta( '_fedistream_available_formats', true );
|
||||
|
||||
return is_array( $formats ) ? $formats : array( 'mp3' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track duration in seconds.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_duration(): int {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) get_post_meta( $track_id, '_fedistream_duration', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted duration.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_formatted_duration(): string {
|
||||
$seconds = $this->get_duration();
|
||||
|
||||
if ( ! $seconds ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$mins = floor( $seconds / 60 );
|
||||
$secs = $seconds % 60;
|
||||
|
||||
return sprintf( '%d:%02d', $mins, $secs );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get artist name(s).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_artist_name(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$artist_ids = get_post_meta( $track_id, '_fedistream_artist_ids', true );
|
||||
|
||||
if ( ! is_array( $artist_ids ) || empty( $artist_ids ) ) {
|
||||
// Fall back to album artist.
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$artist_id = $album_id ? get_post_meta( $album_id, '_fedistream_album_artist', true ) : 0;
|
||||
|
||||
if ( $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
return $artist ? $artist->post_title : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$names = array();
|
||||
foreach ( $artist_ids as $artist_id ) {
|
||||
$artist = get_post( $artist_id );
|
||||
if ( $artist ) {
|
||||
$names[] = $artist->post_title;
|
||||
}
|
||||
}
|
||||
|
||||
return implode( ', ', $names );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get album name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_album_name(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
|
||||
if ( ! $album_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$album = get_post( $album_id );
|
||||
|
||||
return $album ? $album->post_title : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track artwork URL.
|
||||
*
|
||||
* Falls back to album artwork if track has none.
|
||||
*
|
||||
* @param string $size Image size.
|
||||
* @return string
|
||||
*/
|
||||
public function get_track_artwork( string $size = 'medium' ): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Try track thumbnail first.
|
||||
$thumbnail_id = get_post_thumbnail_id( $track_id );
|
||||
|
||||
// Fall back to album artwork.
|
||||
if ( ! $thumbnail_id ) {
|
||||
$album_id = get_post_meta( $track_id, '_fedistream_album_id', true );
|
||||
$thumbnail_id = $album_id ? get_post_thumbnail_id( $album_id ) : 0;
|
||||
}
|
||||
|
||||
if ( ! $thumbnail_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$image = wp_get_attachment_image_url( $thumbnail_id, $size );
|
||||
|
||||
return $image ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audio file URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_audio_url(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$audio_id = get_post_meta( $track_id, '_fedistream_audio_file', true );
|
||||
|
||||
if ( ! $audio_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return wp_get_attachment_url( $audio_id ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if track is explicit.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_explicit(): bool {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) get_post_meta( $track_id, '_fedistream_explicit', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track BPM.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_bpm(): int {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) get_post_meta( $track_id, '_fedistream_bpm', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track musical key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_musical_key(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $track_id, '_fedistream_key', true ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISRC code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_isrc(): string {
|
||||
$track_id = $this->get_linked_track_id();
|
||||
|
||||
if ( ! $track_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return get_post_meta( $track_id, '_fedistream_isrc', true ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get downloads for this product.
|
||||
*
|
||||
* Generates downloadable files based on available formats.
|
||||
*
|
||||
* @param string $context View or edit context.
|
||||
* @return array
|
||||
*/
|
||||
public function get_downloads( $context = 'view' ): array {
|
||||
$downloads = parent::get_downloads( $context );
|
||||
|
||||
// If no manual downloads set, generate from linked track.
|
||||
if ( empty( $downloads ) && $this->get_linked_track_id() ) {
|
||||
$downloads = $this->generate_track_downloads();
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate download files from linked track.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function generate_track_downloads(): array {
|
||||
$downloads = array();
|
||||
$track = $this->get_linked_track();
|
||||
$formats = $this->get_available_formats();
|
||||
|
||||
if ( ! $track ) {
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
// For each format, create a download entry.
|
||||
foreach ( $formats as $format ) {
|
||||
$format_label = strtoupper( $format );
|
||||
$download_id = 'track-' . $track->ID . '-' . $format;
|
||||
|
||||
$downloads[ $download_id ] = array(
|
||||
'id' => $download_id,
|
||||
'name' => sprintf(
|
||||
/* translators: 1: Track name, 2: Format name */
|
||||
__( '%1$s (%2$s)', 'wp-fedistream' ),
|
||||
$track->post_title,
|
||||
$format_label
|
||||
),
|
||||
'file' => add_query_arg(
|
||||
array(
|
||||
'fedistream_download' => 'track',
|
||||
'track_id' => $track->ID,
|
||||
'format' => $format,
|
||||
),
|
||||
home_url( '/' )
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $downloads;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if purchasable.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_purchasable(): bool {
|
||||
// Must have a linked track.
|
||||
if ( ! $this->get_linked_track_id() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check price for fixed pricing.
|
||||
if ( 'fixed' === $this->get_pricing_type() ) {
|
||||
return $this->get_price() !== '' && $this->get_price() >= 0;
|
||||
}
|
||||
|
||||
// PWYW and NYP are always purchasable.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get price HTML.
|
||||
*
|
||||
* @param string $price Price HTML.
|
||||
* @return string
|
||||
*/
|
||||
public function get_price_html( $price = '' ): string {
|
||||
$pricing_type = $this->get_pricing_type();
|
||||
|
||||
if ( 'nyp' === $pricing_type ) {
|
||||
return '<span class="fedistream-nyp-price">' . esc_html__( 'Name Your Price', 'wp-fedistream' ) . '</span>';
|
||||
}
|
||||
|
||||
if ( 'pwyw' === $pricing_type ) {
|
||||
$min_price = $this->get_min_price();
|
||||
$suggested = $this->get_suggested_price();
|
||||
|
||||
$html = '<span class="fedistream-pwyw-price">';
|
||||
|
||||
if ( $min_price > 0 ) {
|
||||
$html .= sprintf(
|
||||
/* translators: %s: Minimum price */
|
||||
esc_html__( 'From %s', 'wp-fedistream' ),
|
||||
wc_price( $min_price )
|
||||
);
|
||||
} else {
|
||||
$html .= esc_html__( 'Pay What You Want', 'wp-fedistream' );
|
||||
}
|
||||
|
||||
if ( $suggested > 0 ) {
|
||||
$html .= ' <span class="fedistream-suggested">';
|
||||
$html .= sprintf(
|
||||
/* translators: %s: Suggested price */
|
||||
esc_html__( '(Suggested: %s)', 'wp-fedistream' ),
|
||||
wc_price( $suggested )
|
||||
);
|
||||
$html .= '</span>';
|
||||
}
|
||||
|
||||
$html .= '</span>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
return parent::get_price_html( $price );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to cart validation for PWYW products.
|
||||
*
|
||||
* @param bool $passed Validation passed.
|
||||
* @param int $product_id Product ID.
|
||||
* @param int $quantity Quantity.
|
||||
* @return bool
|
||||
*/
|
||||
public static function validate_add_to_cart( bool $passed, int $product_id, int $quantity ): bool {
|
||||
$product = wc_get_product( $product_id );
|
||||
|
||||
if ( ! $product || 'fedistream_track' !== $product->get_type() ) {
|
||||
return $passed;
|
||||
}
|
||||
|
||||
$pricing_type = $product->get_pricing_type();
|
||||
|
||||
if ( 'pwyw' === $pricing_type || 'nyp' === $pricing_type ) {
|
||||
$custom_price = isset( $_POST['fedistream_custom_price'] ) ? // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['fedistream_custom_price'] ) ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Missing
|
||||
0;
|
||||
|
||||
$min_price = $product->get_min_price();
|
||||
|
||||
if ( 'pwyw' === $pricing_type && $custom_price < $min_price ) {
|
||||
wc_add_notice(
|
||||
sprintf(
|
||||
/* translators: %s: Minimum price */
|
||||
__( 'Please enter at least %s', 'wp-fedistream' ),
|
||||
wc_price( $min_price )
|
||||
),
|
||||
'error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
WC()->session->set( 'fedistream_custom_price_' . $product_id, $custom_price );
|
||||
}
|
||||
|
||||
return $passed;
|
||||
}
|
||||
}
|
||||
1
includes/index.php
Normal file
1
includes/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
8
index.php
Normal file
8
index.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* Silence is golden.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
// Prevent direct file access.
|
||||
1
languages/index.php
Normal file
1
languages/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
31
languages/wp-fedistream.pot
Normal file
31
languages/wp-fedistream.pot
Normal file
@@ -0,0 +1,31 @@
|
||||
# Copyright (C) 2025 Marco Graetsch
|
||||
# This file is distributed under the same license as the WP FediStream plugin.
|
||||
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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \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."
|
||||
msgstr ""
|
||||
|
||||
#: wp-fedistream.php:105
|
||||
msgid "WP FediStream requires WordPress version %1$s or higher. You are running WordPress %2$s."
|
||||
msgstr ""
|
||||
|
||||
#: wp-fedistream.php:114
|
||||
msgid "WP FediStream requires Composer dependencies to be installed. Please run \"composer install\" in the plugin directory."
|
||||
msgstr ""
|
||||
|
||||
#: wp-fedistream.php:127
|
||||
msgid "WP FediStream requires PHP version %s or higher."
|
||||
msgstr ""
|
||||
27
templates/archive/album.twig
Normal file
27
templates/archive/album.twig
Normal file
@@ -0,0 +1,27 @@
|
||||
{# Album archive template #}
|
||||
<div class="fedistream-archive fedistream-archive--albums">
|
||||
<header class="fedistream-archive__header">
|
||||
<h1 class="fedistream-archive__title">{{ __('Albums', 'wp-fedistream') }}</h1>
|
||||
{% if archive_description %}
|
||||
<div class="fedistream-archive__description">{{ archive_description }}</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if posts is not empty %}
|
||||
<div class="fedistream-grid fedistream-grid--albums">
|
||||
{% for post in posts %}
|
||||
{% include 'partials/card-album.twig' with { post: post } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if pagination %}
|
||||
<nav class="fedistream-pagination">
|
||||
{{ pagination|raw }}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('No albums found.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
27
templates/archive/artist.twig
Normal file
27
templates/archive/artist.twig
Normal file
@@ -0,0 +1,27 @@
|
||||
{# Artist archive template #}
|
||||
<div class="fedistream-archive fedistream-archive--artists">
|
||||
<header class="fedistream-archive__header">
|
||||
<h1 class="fedistream-archive__title">{{ __('Artists', 'wp-fedistream') }}</h1>
|
||||
{% if archive_description %}
|
||||
<div class="fedistream-archive__description">{{ archive_description }}</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if posts is not empty %}
|
||||
<div class="fedistream-grid fedistream-grid--artists">
|
||||
{% for post in posts %}
|
||||
{% include 'partials/card-artist.twig' with { post: post } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if pagination %}
|
||||
<nav class="fedistream-pagination">
|
||||
{{ pagination|raw }}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('No artists found.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
27
templates/archive/playlist.twig
Normal file
27
templates/archive/playlist.twig
Normal file
@@ -0,0 +1,27 @@
|
||||
{# Playlist archive template #}
|
||||
<div class="fedistream-archive fedistream-archive--playlists">
|
||||
<header class="fedistream-archive__header">
|
||||
<h1 class="fedistream-archive__title">{{ __('Playlists', 'wp-fedistream') }}</h1>
|
||||
{% if archive_description %}
|
||||
<div class="fedistream-archive__description">{{ archive_description }}</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if posts is not empty %}
|
||||
<div class="fedistream-grid fedistream-grid--playlists">
|
||||
{% for post in posts %}
|
||||
{% include 'partials/card-playlist.twig' with { post: post } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if pagination %}
|
||||
<nav class="fedistream-pagination">
|
||||
{{ pagination|raw }}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('No playlists found.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
37
templates/archive/taxonomy.twig
Normal file
37
templates/archive/taxonomy.twig
Normal file
@@ -0,0 +1,37 @@
|
||||
{# Taxonomy archive template (Genre, Mood) #}
|
||||
<div class="fedistream-archive fedistream-archive--taxonomy">
|
||||
<header class="fedistream-archive__header">
|
||||
<h1 class="fedistream-archive__title">
|
||||
{% if taxonomy_name %}{{ taxonomy_name }}: {% endif %}{{ term.name }}
|
||||
</h1>
|
||||
{% if term.description %}
|
||||
<div class="fedistream-archive__description">{{ term.description }}</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if posts is not empty %}
|
||||
<div class="fedistream-grid fedistream-grid--mixed">
|
||||
{% for post in posts %}
|
||||
{% if post.post_type == 'fedistream_artist' %}
|
||||
{% include 'partials/card-artist.twig' with { post: post } %}
|
||||
{% elseif post.post_type == 'fedistream_album' %}
|
||||
{% include 'partials/card-album.twig' with { post: post } %}
|
||||
{% elseif post.post_type == 'fedistream_track' %}
|
||||
{% include 'partials/card-track.twig' with { post: post } %}
|
||||
{% elseif post.post_type == 'fedistream_playlist' %}
|
||||
{% include 'partials/card-playlist.twig' with { post: post } %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if pagination %}
|
||||
<nav class="fedistream-pagination">
|
||||
{{ pagination|raw }}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('No content found in this category.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
27
templates/archive/track.twig
Normal file
27
templates/archive/track.twig
Normal file
@@ -0,0 +1,27 @@
|
||||
{# Track archive template #}
|
||||
<div class="fedistream-archive fedistream-archive--tracks">
|
||||
<header class="fedistream-archive__header">
|
||||
<h1 class="fedistream-archive__title">{{ __('Tracks', 'wp-fedistream') }}</h1>
|
||||
{% if archive_description %}
|
||||
<div class="fedistream-archive__description">{{ archive_description }}</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% if posts is not empty %}
|
||||
<div class="fedistream-grid fedistream-grid--tracks">
|
||||
{% for post in posts %}
|
||||
{% include 'partials/card-track.twig' with { post: post } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if pagination %}
|
||||
<nav class="fedistream-pagination">
|
||||
{{ pagination|raw }}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('No tracks found.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
1
templates/index.php
Normal file
1
templates/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?php // Silence is golden.
|
||||
31
templates/partials/card-album.twig
Normal file
31
templates/partials/card-album.twig
Normal file
@@ -0,0 +1,31 @@
|
||||
{# Album card partial #}
|
||||
<article class="fedistream-card fedistream-card--album">
|
||||
<a href="{{ post.permalink }}" class="fedistream-card__link">
|
||||
<div class="fedistream-card__image fedistream-card__image--square">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" loading="lazy">
|
||||
{% else %}
|
||||
<div class="fedistream-card__placeholder fedistream-card__placeholder--album">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 14.5c-2.49 0-4.5-2.01-4.5-4.5S9.51 7.5 12 7.5s4.5 2.01 4.5 4.5-2.01 4.5-4.5 4.5zm0-5.5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-card__content">
|
||||
<h3 class="fedistream-card__title">{{ post.title }}</h3>
|
||||
{% if post.artist_name %}
|
||||
<p class="fedistream-card__artist">{{ post.artist_name }}</p>
|
||||
{% endif %}
|
||||
<p class="fedistream-card__meta">
|
||||
<span class="fedistream-card__type">{{ post.album_type_label }}</span>
|
||||
{% if post.release_year %}
|
||||
<span class="fedistream-card__year">{{ post.release_year }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if post.total_tracks > 0 %}
|
||||
<p class="fedistream-card__stats">
|
||||
{{ post.total_tracks }} {{ post.total_tracks == 1 ? 'track' : 'tracks' }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
28
templates/partials/card-artist.twig
Normal file
28
templates/partials/card-artist.twig
Normal file
@@ -0,0 +1,28 @@
|
||||
{# Artist card partial #}
|
||||
<article class="fedistream-card fedistream-card--artist">
|
||||
<a href="{{ post.permalink }}" class="fedistream-card__link">
|
||||
<div class="fedistream-card__image">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" loading="lazy">
|
||||
{% else %}
|
||||
<div class="fedistream-card__placeholder fedistream-card__placeholder--artist">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-card__content">
|
||||
<h3 class="fedistream-card__title">{{ post.title }}</h3>
|
||||
<p class="fedistream-card__meta">
|
||||
<span class="fedistream-card__type">{{ post.artist_type_label }}</span>
|
||||
{% if post.location %}
|
||||
<span class="fedistream-card__location">{{ post.location }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if post.album_count is defined and post.album_count > 0 %}
|
||||
<p class="fedistream-card__stats">
|
||||
{{ post.album_count }} {{ post.album_count == 1 ? 'album' : 'albums' }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
29
templates/partials/card-playlist.twig
Normal file
29
templates/partials/card-playlist.twig
Normal file
@@ -0,0 +1,29 @@
|
||||
{# Playlist card partial #}
|
||||
<article class="fedistream-card fedistream-card--playlist">
|
||||
<a href="{{ post.permalink }}" class="fedistream-card__link">
|
||||
<div class="fedistream-card__image fedistream-card__image--square">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" loading="lazy">
|
||||
{% else %}
|
||||
<div class="fedistream-card__placeholder fedistream-card__placeholder--playlist">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15 6H3v2h12V6zm0 4H3v2h12v-2zM3 16h8v-2H3v2zM17 6v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6h-5z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.visibility == 'private' %}
|
||||
<span class="fedistream-card__badge fedistream-card__badge--private">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="12" height="12"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-card__content">
|
||||
<h3 class="fedistream-card__title">{{ post.title }}</h3>
|
||||
<p class="fedistream-card__author">{{ post.author }}</p>
|
||||
<p class="fedistream-card__meta">
|
||||
<span class="fedistream-card__count">{{ post.track_count }} {{ post.track_count == 1 ? 'track' : 'tracks' }}</span>
|
||||
{% if post.duration_formatted %}
|
||||
<span class="fedistream-card__duration">{{ post.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
37
templates/partials/card-track.twig
Normal file
37
templates/partials/card-track.twig
Normal file
@@ -0,0 +1,37 @@
|
||||
{# Track card partial #}
|
||||
<article class="fedistream-card fedistream-card--track">
|
||||
<a href="{{ post.permalink }}" class="fedistream-card__link">
|
||||
<div class="fedistream-card__image fedistream-card__image--square">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" loading="lazy">
|
||||
{% elseif post.album_artwork %}
|
||||
<img src="{{ post.album_artwork }}" alt="{{ post.title|e('html_attr') }}" loading="lazy">
|
||||
{% else %}
|
||||
<div class="fedistream-card__placeholder fedistream-card__placeholder--track">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.explicit %}
|
||||
<span class="fedistream-card__badge fedistream-card__badge--explicit">E</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-card__content">
|
||||
<h3 class="fedistream-card__title">{{ post.title }}</h3>
|
||||
{% if post.artists %}
|
||||
<p class="fedistream-card__artist">
|
||||
{% for artist in post.artists %}
|
||||
{{ artist.name }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="fedistream-card__meta">
|
||||
{% if post.album_title %}
|
||||
<span class="fedistream-card__album">{{ post.album_title }}</span>
|
||||
{% endif %}
|
||||
{% if post.duration_formatted %}
|
||||
<span class="fedistream-card__duration">{{ post.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
64
templates/shortcodes/album.twig
Normal file
64
templates/shortcodes/album.twig
Normal file
@@ -0,0 +1,64 @@
|
||||
{# Album shortcode template #}
|
||||
<div class="fedistream-shortcode fedistream-shortcode--album fedistream-shortcode--{{ layout }}">
|
||||
<div class="fedistream-album">
|
||||
<div class="fedistream-album__header">
|
||||
<div class="fedistream-album__artwork">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-album__image">
|
||||
{% else %}
|
||||
<div class="fedistream-album__placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 14.5c-2.49 0-4.5-2.01-4.5-4.5S9.51 7.5 12 7.5s4.5 2.01 4.5 4.5-2.01 4.5-4.5 4.5zm0-5.5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-album__info">
|
||||
{% if post.album_type %}
|
||||
<span class="fedistream-album__type-badge">{{ post.album_type }}</span>
|
||||
{% endif %}
|
||||
<h3 class="fedistream-album__title">
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
{% if post.artist %}
|
||||
<p class="fedistream-album__artist">
|
||||
<a href="{{ post.artist_link }}">{{ post.artist }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="fedistream-album__meta">
|
||||
{% if post.release_date %}
|
||||
<span class="fedistream-album__date">{{ post.release_date }}</span>
|
||||
{% endif %}
|
||||
{% if post.track_count %}
|
||||
<span class="fedistream-album__tracks">{{ post.track_count }} {{ post.track_count == 1 ? 'track' : 'tracks' }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-album__actions">
|
||||
<button type="button" class="fedistream-btn fedistream-btn--primary fedistream-btn--play-all" data-album-id="{{ post.id }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
{{ __('Play', 'wp-fedistream') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if show_tracks and post.tracks is not empty %}
|
||||
<div class="fedistream-album__tracklist">
|
||||
<div class="fedistream-tracklist">
|
||||
{% for track in post.tracks %}
|
||||
<div class="fedistream-tracklist__item" data-track-id="{{ track.id }}">
|
||||
<span class="fedistream-tracklist__number">{{ track.track_number|default(loop.index) }}</span>
|
||||
<div class="fedistream-tracklist__info">
|
||||
<a href="{{ track.permalink }}" class="fedistream-tracklist__title">{{ track.title }}</a>
|
||||
</div>
|
||||
{% if track.duration_formatted %}
|
||||
<span class="fedistream-tracklist__duration">{{ track.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
<button type="button" class="fedistream-tracklist__play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
65
templates/shortcodes/artist.twig
Normal file
65
templates/shortcodes/artist.twig
Normal file
@@ -0,0 +1,65 @@
|
||||
{# Artist shortcode template #}
|
||||
<div class="fedistream-shortcode fedistream-shortcode--artist fedistream-shortcode--{{ layout }}">
|
||||
<div class="fedistream-artist">
|
||||
<div class="fedistream-artist__header">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-artist__image">
|
||||
{% else %}
|
||||
<div class="fedistream-artist__placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="fedistream-artist__info">
|
||||
<h3 class="fedistream-artist__name">
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
{% if post.artist_type %}
|
||||
<span class="fedistream-artist__type">{{ post.artist_type }}</span>
|
||||
{% endif %}
|
||||
{% if post.genres is not empty %}
|
||||
<div class="fedistream-artist__genres">
|
||||
{% for genre in post.genres %}
|
||||
<a href="{{ genre.link }}" class="fedistream-tag">{{ genre.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if layout == 'full' and post.content %}
|
||||
<div class="fedistream-artist__bio">
|
||||
{{ post.content|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_albums and post.albums is not empty %}
|
||||
<div class="fedistream-artist__albums">
|
||||
<h4 class="fedistream-section__title">{{ __('Albums', 'wp-fedistream') }}</h4>
|
||||
<div class="fedistream-grid fedistream-grid--small">
|
||||
{% for album in post.albums|slice(0, 4) %}
|
||||
{% include 'partials/card-album.twig' with { post: album } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if show_tracks and post.tracks is not empty %}
|
||||
<div class="fedistream-artist__tracks">
|
||||
<h4 class="fedistream-section__title">{{ __('Popular Tracks', 'wp-fedistream') }}</h4>
|
||||
<div class="fedistream-tracklist fedistream-tracklist--compact">
|
||||
{% for track in post.tracks|slice(0, 5) %}
|
||||
<div class="fedistream-tracklist__item" data-track-id="{{ track.id }}">
|
||||
<span class="fedistream-tracklist__number">{{ loop.index }}</span>
|
||||
<div class="fedistream-tracklist__info">
|
||||
<a href="{{ track.permalink }}" class="fedistream-tracklist__title">{{ track.title }}</a>
|
||||
</div>
|
||||
{% if track.duration_formatted %}
|
||||
<span class="fedistream-tracklist__duration">{{ track.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
18
templates/shortcodes/artists-grid.twig
Normal file
18
templates/shortcodes/artists-grid.twig
Normal file
@@ -0,0 +1,18 @@
|
||||
{# Artists grid shortcode template #}
|
||||
<div class="fedistream-shortcode fedistream-shortcode--artists">
|
||||
{% if title %}
|
||||
<h3 class="fedistream-shortcode__title">{{ title }}</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if posts is not empty %}
|
||||
<div class="fedistream-grid fedistream-grid--artists fedistream-grid--cols-{{ columns }}">
|
||||
{% for post in posts %}
|
||||
{% include 'partials/card-artist.twig' with { post: post } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('No artists found.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
108
templates/shortcodes/player.twig
Normal file
108
templates/shortcodes/player.twig
Normal file
@@ -0,0 +1,108 @@
|
||||
{# Audio player shortcode template #}
|
||||
<div class="fedistream-shortcode fedistream-shortcode--player fedistream-player-widget fedistream-player-widget--{{ style }}" data-autoplay="{{ autoplay ? 'true' : 'false' }}">
|
||||
{% if tracks|length == 1 %}
|
||||
{# Single track player #}
|
||||
{% set track = tracks[0] %}
|
||||
<div class="fedistream-player fedistream-player--single" data-track-id="{{ track.id }}" data-audio-url="{{ track.audio_url }}">
|
||||
<div class="fedistream-player__track-info">
|
||||
{% if track.thumbnail %}
|
||||
<img src="{{ track.thumbnail }}" alt="{{ track.title|e('html_attr') }}" class="fedistream-player__artwork">
|
||||
{% endif %}
|
||||
<div class="fedistream-player__details">
|
||||
<span class="fedistream-player__title">{{ track.title }}</span>
|
||||
{% if track.artist %}
|
||||
<span class="fedistream-player__artist">{{ track.artist }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="fedistream-player__controls">
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg class="fedistream-player__icon fedistream-player__icon--play" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg class="fedistream-player__icon fedistream-player__icon--pause" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="fedistream-player__progress">
|
||||
<span class="fedistream-player__time fedistream-player__time--current">0:00</span>
|
||||
<div class="fedistream-player__bar">
|
||||
<div class="fedistream-player__bar-progress"></div>
|
||||
<input type="range" class="fedistream-player__seek" min="0" max="100" value="0" aria-label="{{ __('Seek', 'wp-fedistream') }}">
|
||||
</div>
|
||||
<span class="fedistream-player__time fedistream-player__time--total">{{ track.duration_formatted|default('0:00') }}</span>
|
||||
</div>
|
||||
<div class="fedistream-player__volume">
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--volume" aria-label="{{ __('Volume', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
|
||||
</button>
|
||||
<input type="range" class="fedistream-player__volume-slider" min="0" max="100" value="80" aria-label="{{ __('Volume', 'wp-fedistream') }}">
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{# Multi-track player (playlist/album) #}
|
||||
<div class="fedistream-player fedistream-player--multi" data-tracks="{{ tracks|json_encode|e('html_attr') }}">
|
||||
<div class="fedistream-player__now-playing">
|
||||
<div class="fedistream-player__artwork-wrapper">
|
||||
<img src="" alt="" class="fedistream-player__artwork fedistream-player__artwork--current">
|
||||
</div>
|
||||
<div class="fedistream-player__details">
|
||||
<span class="fedistream-player__title fedistream-player__title--current"></span>
|
||||
<span class="fedistream-player__artist fedistream-player__artist--current"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fedistream-player__main-controls">
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--prev" aria-label="{{ __('Previous', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--play fedistream-player__btn--play-main" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg class="fedistream-player__icon fedistream-player__icon--play" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg class="fedistream-player__icon fedistream-player__icon--pause" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--next" aria-label="{{ __('Next', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="fedistream-player__progress">
|
||||
<span class="fedistream-player__time fedistream-player__time--current">0:00</span>
|
||||
<div class="fedistream-player__bar">
|
||||
<div class="fedistream-player__bar-progress"></div>
|
||||
<input type="range" class="fedistream-player__seek" min="0" max="100" value="0" aria-label="{{ __('Seek', 'wp-fedistream') }}">
|
||||
</div>
|
||||
<span class="fedistream-player__time fedistream-player__time--total">0:00</span>
|
||||
</div>
|
||||
<div class="fedistream-player__secondary-controls">
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--shuffle" aria-label="{{ __('Shuffle', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--repeat" aria-label="{{ __('Repeat', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z"/></svg>
|
||||
</button>
|
||||
<div class="fedistream-player__volume">
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--volume" aria-label="{{ __('Volume', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
|
||||
</button>
|
||||
<input type="range" class="fedistream-player__volume-slider" min="0" max="100" value="80" aria-label="{{ __('Volume', 'wp-fedistream') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Track list #}
|
||||
<div class="fedistream-player__queue">
|
||||
<div class="fedistream-tracklist fedistream-tracklist--queue">
|
||||
{% for track in tracks %}
|
||||
<div class="fedistream-tracklist__item" data-track-index="{{ loop.index0 }}" data-track-id="{{ track.id }}">
|
||||
<span class="fedistream-tracklist__number">{{ loop.index }}</span>
|
||||
{% if track.thumbnail %}
|
||||
<img src="{{ track.thumbnail }}" alt="" class="fedistream-tracklist__artwork">
|
||||
{% endif %}
|
||||
<div class="fedistream-tracklist__info">
|
||||
<span class="fedistream-tracklist__title">{{ track.title }}</span>
|
||||
<span class="fedistream-tracklist__artist">{{ track.artist }}</span>
|
||||
</div>
|
||||
{% if track.duration_formatted %}
|
||||
<span class="fedistream-tracklist__duration">{{ track.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
71
templates/shortcodes/playlist.twig
Normal file
71
templates/shortcodes/playlist.twig
Normal file
@@ -0,0 +1,71 @@
|
||||
{# Playlist shortcode template #}
|
||||
<div class="fedistream-shortcode fedistream-shortcode--playlist fedistream-shortcode--{{ layout }}">
|
||||
<div class="fedistream-playlist">
|
||||
<div class="fedistream-playlist__header">
|
||||
<div class="fedistream-playlist__artwork">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-playlist__image">
|
||||
{% else %}
|
||||
<div class="fedistream-playlist__placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15 6H3v2h12V6zm0 4H3v2h12v-2zM3 16h8v-2H3v2zM17 6v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6h-5z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.visibility == 'private' %}
|
||||
<span class="fedistream-playlist__badge fedistream-playlist__badge--private">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="12" height="12"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-playlist__info">
|
||||
<span class="fedistream-playlist__type-badge">{{ __('Playlist', 'wp-fedistream') }}</span>
|
||||
<h3 class="fedistream-playlist__title">
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
{% if post.author %}
|
||||
<p class="fedistream-playlist__author">
|
||||
{{ __('by', 'wp-fedistream') }} {{ post.author }}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="fedistream-playlist__meta">
|
||||
{% if post.track_count %}
|
||||
<span class="fedistream-playlist__count">{{ post.track_count }} {{ post.track_count == 1 ? 'track' : 'tracks' }}</span>
|
||||
{% endif %}
|
||||
{% if post.duration_formatted %}
|
||||
<span class="fedistream-playlist__duration">{{ post.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-playlist__actions">
|
||||
<button type="button" class="fedistream-btn fedistream-btn--primary fedistream-btn--play-all" data-playlist-id="{{ post.id }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
{{ __('Play', 'wp-fedistream') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if show_tracks and post.tracks is not empty %}
|
||||
<div class="fedistream-playlist__tracklist">
|
||||
<div class="fedistream-tracklist">
|
||||
{% for track in post.tracks %}
|
||||
<div class="fedistream-tracklist__item" data-track-id="{{ track.id }}">
|
||||
<span class="fedistream-tracklist__number">{{ loop.index }}</span>
|
||||
{% if track.thumbnail %}
|
||||
<img src="{{ track.thumbnail }}" alt="" class="fedistream-tracklist__artwork">
|
||||
{% endif %}
|
||||
<div class="fedistream-tracklist__info">
|
||||
<a href="{{ track.permalink }}" class="fedistream-tracklist__title">{{ track.title }}</a>
|
||||
<span class="fedistream-tracklist__artist">{{ track.artist }}</span>
|
||||
</div>
|
||||
{% if track.duration_formatted %}
|
||||
<span class="fedistream-tracklist__duration">{{ track.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
<button type="button" class="fedistream-tracklist__play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
18
templates/shortcodes/releases-grid.twig
Normal file
18
templates/shortcodes/releases-grid.twig
Normal file
@@ -0,0 +1,18 @@
|
||||
{# Latest releases grid shortcode template #}
|
||||
<div class="fedistream-shortcode fedistream-shortcode--releases">
|
||||
{% if title %}
|
||||
<h3 class="fedistream-shortcode__title">{{ title }}</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if posts is not empty %}
|
||||
<div class="fedistream-grid fedistream-grid--albums fedistream-grid--cols-{{ columns }}">
|
||||
{% for post in posts %}
|
||||
{% include 'partials/card-album.twig' with { post: post } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('No releases found.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
64
templates/shortcodes/track.twig
Normal file
64
templates/shortcodes/track.twig
Normal file
@@ -0,0 +1,64 @@
|
||||
{# Track shortcode template #}
|
||||
<div class="fedistream-shortcode fedistream-shortcode--track fedistream-shortcode--{{ layout }}">
|
||||
<div class="fedistream-track">
|
||||
<div class="fedistream-track__header">
|
||||
<div class="fedistream-track__artwork">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-track__image">
|
||||
{% else %}
|
||||
<div class="fedistream-track__placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if show_player %}
|
||||
<button type="button" class="fedistream-track__play-overlay" data-track-id="{{ post.id }}" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-track__info">
|
||||
<h3 class="fedistream-track__title">
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
{% if post.artists is not empty %}
|
||||
<p class="fedistream-track__artists">
|
||||
{% for artist in post.artists %}
|
||||
<a href="{{ artist.link }}">{{ artist.name }}</a>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if post.album %}
|
||||
<p class="fedistream-track__album">
|
||||
{{ __('From', 'wp-fedistream') }} <a href="{{ post.album_link }}">{{ post.album }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="fedistream-track__meta">
|
||||
{% if post.duration_formatted %}
|
||||
<span class="fedistream-track__duration">{{ post.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
{% if post.play_count %}
|
||||
<span class="fedistream-track__plays">{{ post.play_count }} {{ post.play_count == 1 ? 'play' : 'plays' }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if show_player and post.audio_url %}
|
||||
<div class="fedistream-track__player">
|
||||
<div class="fedistream-player fedistream-player--inline" data-track-id="{{ post.id }}" data-audio-url="{{ post.audio_url }}">
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg class="fedistream-player__icon fedistream-player__icon--play" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg class="fedistream-player__icon fedistream-player__icon--pause" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||
</button>
|
||||
<div class="fedistream-player__progress">
|
||||
<div class="fedistream-player__bar">
|
||||
<div class="fedistream-player__bar-progress"></div>
|
||||
<input type="range" class="fedistream-player__seek" min="0" max="100" value="0" aria-label="{{ __('Seek', 'wp-fedistream') }}">
|
||||
</div>
|
||||
</div>
|
||||
<span class="fedistream-player__time">{{ post.duration_formatted|default('0:00') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
48
templates/shortcodes/tracks-list.twig
Normal file
48
templates/shortcodes/tracks-list.twig
Normal file
@@ -0,0 +1,48 @@
|
||||
{# Popular tracks list shortcode template #}
|
||||
<div class="fedistream-shortcode fedistream-shortcode--tracks">
|
||||
{% if title %}
|
||||
<h3 class="fedistream-shortcode__title">{{ title }}</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if posts is not empty %}
|
||||
<div class="fedistream-tracklist fedistream-tracklist--numbered">
|
||||
{% for post in posts %}
|
||||
<div class="fedistream-tracklist__item" data-track-id="{{ post.id }}">
|
||||
<span class="fedistream-tracklist__rank">{{ loop.index }}</span>
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="" class="fedistream-tracklist__artwork">
|
||||
{% else %}
|
||||
<div class="fedistream-tracklist__artwork fedistream-tracklist__artwork--placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="fedistream-tracklist__info">
|
||||
<a href="{{ post.permalink }}" class="fedistream-tracklist__title">{{ post.title }}</a>
|
||||
<span class="fedistream-tracklist__artist">
|
||||
{% if post.artists is iterable %}
|
||||
{% for artist in post.artists %}
|
||||
<a href="{{ artist.link }}">{{ artist.name }}</a>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{ post.artist }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if post.play_count %}
|
||||
<span class="fedistream-tracklist__plays">{{ post.play_count|number_format }}</span>
|
||||
{% endif %}
|
||||
{% if post.duration_formatted %}
|
||||
<span class="fedistream-tracklist__duration">{{ post.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
<button type="button" class="fedistream-tracklist__play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('No tracks found.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
105
templates/single/album.twig
Normal file
105
templates/single/album.twig
Normal file
@@ -0,0 +1,105 @@
|
||||
{# Single album template #}
|
||||
<article class="fedistream-single fedistream-single--album">
|
||||
<header class="fedistream-single__header fedistream-single__header--album">
|
||||
<div class="fedistream-single__artwork">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-single__image fedistream-single__image--album">
|
||||
{% else %}
|
||||
<div class="fedistream-single__placeholder fedistream-single__placeholder--album">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 14.5c-2.49 0-4.5-2.01-4.5-4.5S9.51 7.5 12 7.5s4.5 2.01 4.5 4.5-2.01 4.5-4.5 4.5zm0-5.5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-single__info">
|
||||
<span class="fedistream-single__type-badge">{{ post.album_type|default('Album') }}</span>
|
||||
<h1 class="fedistream-single__title">{{ post.title }}</h1>
|
||||
{% if post.artist %}
|
||||
<p class="fedistream-single__artist">
|
||||
<a href="{{ post.artist_link }}">{{ post.artist }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="fedistream-single__meta">
|
||||
{% if post.release_date %}
|
||||
<span class="fedistream-single__date">{{ post.release_date }}</span>
|
||||
{% endif %}
|
||||
{% if post.track_count %}
|
||||
<span class="fedistream-single__tracks">{{ post.track_count }} {{ post.track_count == 1 ? __('track', 'wp-fedistream') : __('tracks', 'wp-fedistream') }}</span>
|
||||
{% endif %}
|
||||
{% if post.duration_formatted %}
|
||||
<span class="fedistream-single__duration">{{ post.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if post.genres is not empty %}
|
||||
<div class="fedistream-single__genres">
|
||||
{% for genre in post.genres %}
|
||||
<a href="{{ genre.link }}" class="fedistream-tag">{{ genre.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="fedistream-single__actions">
|
||||
<button type="button" class="fedistream-btn fedistream-btn--primary fedistream-btn--play-all" data-album-id="{{ post.id }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
{{ __('Play All', 'wp-fedistream') }}
|
||||
</button>
|
||||
<button type="button" class="fedistream-btn fedistream-btn--secondary fedistream-btn--shuffle" data-album-id="{{ post.id }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/></svg>
|
||||
{{ __('Shuffle', 'wp-fedistream') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if post.tracks is not empty %}
|
||||
<section class="fedistream-single__tracklist">
|
||||
<div class="fedistream-tracklist fedistream-tracklist--album">
|
||||
{% for track in post.tracks %}
|
||||
<div class="fedistream-tracklist__item" data-track-id="{{ track.id }}">
|
||||
<span class="fedistream-tracklist__number">{{ track.track_number|default(loop.index) }}</span>
|
||||
<div class="fedistream-tracklist__info">
|
||||
<a href="{{ track.permalink }}" class="fedistream-tracklist__title">{{ track.title }}</a>
|
||||
{% if track.featured_artists %}
|
||||
<span class="fedistream-tracklist__featuring">{{ __('feat.', 'wp-fedistream') }} {{ track.featured_artists }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if track.explicit %}
|
||||
<span class="fedistream-badge fedistream-badge--explicit">E</span>
|
||||
{% endif %}
|
||||
{% if track.duration_formatted %}
|
||||
<span class="fedistream-tracklist__duration">{{ track.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
<button type="button" class="fedistream-tracklist__play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.content %}
|
||||
<section class="fedistream-single__content">
|
||||
<h2 class="fedistream-section__title">{{ __('About This Album', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-single__description">
|
||||
{{ post.content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.credits %}
|
||||
<section class="fedistream-single__credits">
|
||||
<h2 class="fedistream-section__title">{{ __('Credits', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-credits">
|
||||
{{ post.credits|raw }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.license %}
|
||||
<section class="fedistream-single__license">
|
||||
<p class="fedistream-license">
|
||||
<strong>{{ __('License:', 'wp-fedistream') }}</strong>
|
||||
<a href="{{ post.license.link }}">{{ post.license.name }}</a>
|
||||
</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</article>
|
||||
88
templates/single/artist.twig
Normal file
88
templates/single/artist.twig
Normal file
@@ -0,0 +1,88 @@
|
||||
{# Single artist template #}
|
||||
<article class="fedistream-single fedistream-single--artist">
|
||||
<header class="fedistream-single__header">
|
||||
<div class="fedistream-single__hero">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-single__image fedistream-single__image--artist">
|
||||
{% else %}
|
||||
<div class="fedistream-single__placeholder fedistream-single__placeholder--artist">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-single__info">
|
||||
<h1 class="fedistream-single__title">{{ post.title }}</h1>
|
||||
{% if post.artist_type %}
|
||||
<p class="fedistream-single__type">{{ post.artist_type }}</p>
|
||||
{% endif %}
|
||||
{% if post.genres is not empty %}
|
||||
<div class="fedistream-single__genres">
|
||||
{% for genre in post.genres %}
|
||||
<a href="{{ genre.link }}" class="fedistream-tag">{{ genre.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if post.content %}
|
||||
<section class="fedistream-single__content">
|
||||
<h2 class="fedistream-section__title">{{ __('About', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-single__description">
|
||||
{{ post.content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.social_links is not empty %}
|
||||
<section class="fedistream-single__social">
|
||||
<h2 class="fedistream-section__title">{{ __('Connect', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-social-links">
|
||||
{% for platform, url in post.social_links %}
|
||||
<a href="{{ url }}" class="fedistream-social-link fedistream-social-link--{{ platform }}" target="_blank" rel="noopener noreferrer">
|
||||
{{ platform }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.albums is not empty %}
|
||||
<section class="fedistream-single__albums">
|
||||
<h2 class="fedistream-section__title">{{ __('Discography', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-grid fedistream-grid--albums">
|
||||
{% for album in post.albums %}
|
||||
{% include 'partials/card-album.twig' with { post: album } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.tracks is not empty %}
|
||||
<section class="fedistream-single__tracks">
|
||||
<h2 class="fedistream-section__title">{{ __('Popular Tracks', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-tracklist">
|
||||
{% for track in post.tracks %}
|
||||
<div class="fedistream-tracklist__item" data-track-id="{{ track.id }}">
|
||||
<span class="fedistream-tracklist__number">{{ loop.index }}</span>
|
||||
{% if track.thumbnail %}
|
||||
<img src="{{ track.thumbnail }}" alt="" class="fedistream-tracklist__artwork">
|
||||
{% endif %}
|
||||
<div class="fedistream-tracklist__info">
|
||||
<a href="{{ track.permalink }}" class="fedistream-tracklist__title">{{ track.title }}</a>
|
||||
{% if track.album %}
|
||||
<span class="fedistream-tracklist__album">{{ track.album }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if track.duration_formatted %}
|
||||
<span class="fedistream-tracklist__duration">{{ track.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
<button type="button" class="fedistream-tracklist__play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</article>
|
||||
107
templates/single/playlist.twig
Normal file
107
templates/single/playlist.twig
Normal file
@@ -0,0 +1,107 @@
|
||||
{# Single playlist template #}
|
||||
<article class="fedistream-single fedistream-single--playlist">
|
||||
<header class="fedistream-single__header fedistream-single__header--playlist">
|
||||
<div class="fedistream-single__artwork">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-single__image fedistream-single__image--playlist">
|
||||
{% else %}
|
||||
<div class="fedistream-single__placeholder fedistream-single__placeholder--playlist">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15 6H3v2h12V6zm0 4H3v2h12v-2zM3 16h8v-2H3v2zM17 6v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6h-5z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.visibility == 'private' %}
|
||||
<span class="fedistream-single__badge fedistream-single__badge--private">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z"/></svg>
|
||||
{{ __('Private', 'wp-fedistream') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fedistream-single__info">
|
||||
<span class="fedistream-single__type-badge">{{ __('Playlist', 'wp-fedistream') }}</span>
|
||||
<h1 class="fedistream-single__title">{{ post.title }}</h1>
|
||||
{% if post.author %}
|
||||
<p class="fedistream-single__author">
|
||||
{{ __('Created by', 'wp-fedistream') }} <a href="{{ post.author_link }}">{{ post.author }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="fedistream-single__meta">
|
||||
{% if post.track_count %}
|
||||
<span class="fedistream-single__tracks">{{ post.track_count }} {{ post.track_count == 1 ? __('track', 'wp-fedistream') : __('tracks', 'wp-fedistream') }}</span>
|
||||
{% endif %}
|
||||
{% if post.duration_formatted %}
|
||||
<span class="fedistream-single__duration">{{ post.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
{% if post.updated_date %}
|
||||
<span class="fedistream-single__updated">{{ __('Updated', 'wp-fedistream') }} {{ post.updated_date }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if post.moods is not empty %}
|
||||
<div class="fedistream-single__moods">
|
||||
{% for mood in post.moods %}
|
||||
<a href="{{ mood.link }}" class="fedistream-tag fedistream-tag--mood">{{ mood.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="fedistream-single__actions">
|
||||
<button type="button" class="fedistream-btn fedistream-btn--primary fedistream-btn--play-all" data-playlist-id="{{ post.id }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
{{ __('Play All', 'wp-fedistream') }}
|
||||
</button>
|
||||
<button type="button" class="fedistream-btn fedistream-btn--secondary fedistream-btn--shuffle" data-playlist-id="{{ post.id }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z"/></svg>
|
||||
{{ __('Shuffle', 'wp-fedistream') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if post.content %}
|
||||
<section class="fedistream-single__content">
|
||||
<div class="fedistream-single__description">
|
||||
{{ post.content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.tracks is not empty %}
|
||||
<section class="fedistream-single__tracklist">
|
||||
<div class="fedistream-tracklist fedistream-tracklist--playlist">
|
||||
{% for track in post.tracks %}
|
||||
<div class="fedistream-tracklist__item" data-track-id="{{ track.id }}">
|
||||
<span class="fedistream-tracklist__number">{{ loop.index }}</span>
|
||||
{% if track.thumbnail %}
|
||||
<img src="{{ track.thumbnail }}" alt="" class="fedistream-tracklist__artwork">
|
||||
{% endif %}
|
||||
<div class="fedistream-tracklist__info">
|
||||
<a href="{{ track.permalink }}" class="fedistream-tracklist__title">{{ track.title }}</a>
|
||||
<span class="fedistream-tracklist__artist">
|
||||
{% if track.artists is iterable %}
|
||||
{% for artist in track.artists %}
|
||||
<a href="{{ artist.link }}">{{ artist.name }}</a>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{ track.artist }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% if track.explicit %}
|
||||
<span class="fedistream-badge fedistream-badge--explicit">E</span>
|
||||
{% endif %}
|
||||
{% if track.duration_formatted %}
|
||||
<span class="fedistream-tracklist__duration">{{ track.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
<button type="button" class="fedistream-tracklist__play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section class="fedistream-single__empty">
|
||||
<div class="fedistream-empty">
|
||||
<p>{{ __('This playlist is empty.', 'wp-fedistream') }}</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</article>
|
||||
120
templates/single/track.twig
Normal file
120
templates/single/track.twig
Normal file
@@ -0,0 +1,120 @@
|
||||
{# Single track template #}
|
||||
<article class="fedistream-single fedistream-single--track">
|
||||
<header class="fedistream-single__header fedistream-single__header--track">
|
||||
<div class="fedistream-single__artwork">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-single__image fedistream-single__image--track">
|
||||
{% else %}
|
||||
<div class="fedistream-single__placeholder fedistream-single__placeholder--track">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="button" class="fedistream-single__play-overlay" data-track-id="{{ post.id }}" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="fedistream-single__info">
|
||||
<h1 class="fedistream-single__title">{{ post.title }}</h1>
|
||||
{% if post.artists is not empty %}
|
||||
<p class="fedistream-single__artists">
|
||||
{% for artist in post.artists %}
|
||||
<a href="{{ artist.link }}">{{ artist.name }}</a>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if post.album %}
|
||||
<p class="fedistream-single__album">
|
||||
{{ __('From', 'wp-fedistream') }} <a href="{{ post.album_link }}">{{ post.album }}</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="fedistream-single__meta">
|
||||
{% if post.duration_formatted %}
|
||||
<span class="fedistream-single__duration">{{ post.duration_formatted }}</span>
|
||||
{% endif %}
|
||||
{% if post.play_count %}
|
||||
<span class="fedistream-single__plays">{{ post.play_count }} {{ post.play_count == 1 ? __('play', 'wp-fedistream') : __('plays', 'wp-fedistream') }}</span>
|
||||
{% endif %}
|
||||
{% if post.explicit %}
|
||||
<span class="fedistream-badge fedistream-badge--explicit">{{ __('Explicit', 'wp-fedistream') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if post.genres is not empty %}
|
||||
<div class="fedistream-single__genres">
|
||||
{% for genre in post.genres %}
|
||||
<a href="{{ genre.link }}" class="fedistream-tag">{{ genre.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.moods is not empty %}
|
||||
<div class="fedistream-single__moods">
|
||||
{% for mood in post.moods %}
|
||||
<a href="{{ mood.link }}" class="fedistream-tag fedistream-tag--mood">{{ mood.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if post.audio_url %}
|
||||
<section class="fedistream-single__player">
|
||||
<div class="fedistream-player" data-track-id="{{ post.id }}" data-audio-url="{{ post.audio_url }}">
|
||||
<div class="fedistream-player__controls">
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--play" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg class="fedistream-player__icon fedistream-player__icon--play" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg class="fedistream-player__icon fedistream-player__icon--pause" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="fedistream-player__progress">
|
||||
<span class="fedistream-player__time fedistream-player__time--current">0:00</span>
|
||||
<div class="fedistream-player__bar">
|
||||
<div class="fedistream-player__bar-progress"></div>
|
||||
<input type="range" class="fedistream-player__seek" min="0" max="100" value="0" aria-label="{{ __('Seek', 'wp-fedistream') }}">
|
||||
</div>
|
||||
<span class="fedistream-player__time fedistream-player__time--total">{{ post.duration_formatted|default('0:00') }}</span>
|
||||
</div>
|
||||
<div class="fedistream-player__volume">
|
||||
<button type="button" class="fedistream-player__btn fedistream-player__btn--volume" aria-label="{{ __('Volume', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>
|
||||
</button>
|
||||
<input type="range" class="fedistream-player__volume-slider" min="0" max="100" value="80" aria-label="{{ __('Volume', 'wp-fedistream') }}">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.content %}
|
||||
<section class="fedistream-single__content">
|
||||
<h2 class="fedistream-section__title">{{ __('About This Track', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-single__description">
|
||||
{{ post.content|raw }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.lyrics %}
|
||||
<section class="fedistream-single__lyrics">
|
||||
<h2 class="fedistream-section__title">{{ __('Lyrics', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-lyrics">
|
||||
{{ post.lyrics|nl2br }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.credits %}
|
||||
<section class="fedistream-single__credits">
|
||||
<h2 class="fedistream-section__title">{{ __('Credits', 'wp-fedistream') }}</h2>
|
||||
<div class="fedistream-credits">
|
||||
{{ post.credits|raw }}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if post.license %}
|
||||
<section class="fedistream-single__license">
|
||||
<p class="fedistream-license">
|
||||
<strong>{{ __('License:', 'wp-fedistream') }}</strong>
|
||||
<a href="{{ post.license.link }}">{{ post.license.name }}</a>
|
||||
</p>
|
||||
</section>
|
||||
{% endif %}
|
||||
</article>
|
||||
41
templates/widgets/featured-artist.twig
Normal file
41
templates/widgets/featured-artist.twig
Normal file
@@ -0,0 +1,41 @@
|
||||
{# Featured Artist Widget Template #}
|
||||
{% if post %}
|
||||
<div class="fedistream-widget__featured">
|
||||
<a href="{{ post.permalink }}" class="fedistream-widget__featured-link">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-widget__featured-image">
|
||||
{% else %}
|
||||
<span class="fedistream-widget__placeholder fedistream-widget__placeholder--large">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<div class="fedistream-widget__featured-info">
|
||||
<h4 class="fedistream-widget__featured-name">
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</h4>
|
||||
{% if post.artist_type %}
|
||||
<span class="fedistream-widget__featured-type">{{ post.artist_type }}</span>
|
||||
{% endif %}
|
||||
{% if post.genres is not empty %}
|
||||
<div class="fedistream-widget__featured-genres">
|
||||
{% for genre in post.genres|slice(0, 3) %}
|
||||
<a href="{{ genre.link }}" class="fedistream-tag fedistream-tag--small">{{ genre.name }}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if post.album_count or post.track_count %}
|
||||
<div class="fedistream-widget__featured-stats">
|
||||
{% if post.album_count %}
|
||||
<span>{{ post.album_count }} {{ post.album_count == 1 ? __('album', 'wp-fedistream') : __('albums', 'wp-fedistream') }}</span>
|
||||
{% endif %}
|
||||
{% if post.track_count %}
|
||||
<span>{{ post.track_count }} {{ post.track_count == 1 ? __('track', 'wp-fedistream') : __('tracks', 'wp-fedistream') }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="fedistream-widget__empty">{{ __('No artist selected.', 'wp-fedistream') }}</p>
|
||||
{% endif %}
|
||||
41
templates/widgets/now-playing.twig
Normal file
41
templates/widgets/now-playing.twig
Normal file
@@ -0,0 +1,41 @@
|
||||
{# Now Playing Widget Template #}
|
||||
<div class="fedistream-now-playing" data-widget="now-playing">
|
||||
<div class="fedistream-now-playing__idle">
|
||||
<span class="fedistream-now-playing__placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
||||
</span>
|
||||
<span class="fedistream-now-playing__message">{{ __('Nothing playing', 'wp-fedistream') }}</span>
|
||||
</div>
|
||||
<div class="fedistream-now-playing__active" style="display: none;">
|
||||
<div class="fedistream-now-playing__track">
|
||||
<img src="" alt="" class="fedistream-now-playing__artwork">
|
||||
<div class="fedistream-now-playing__info">
|
||||
<span class="fedistream-now-playing__title"></span>
|
||||
<span class="fedistream-now-playing__artist"></span>
|
||||
</div>
|
||||
</div>
|
||||
{% if show_player %}
|
||||
<div class="fedistream-now-playing__controls">
|
||||
<button type="button" class="fedistream-now-playing__btn fedistream-now-playing__btn--prev" aria-label="{{ __('Previous', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="fedistream-now-playing__btn fedistream-now-playing__btn--play" aria-label="{{ __('Play/Pause', 'wp-fedistream') }}">
|
||||
<svg class="fedistream-now-playing__icon fedistream-now-playing__icon--play" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
<svg class="fedistream-now-playing__icon fedistream-now-playing__icon--pause" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
||||
</button>
|
||||
<button type="button" class="fedistream-now-playing__btn fedistream-now-playing__btn--next" aria-label="{{ __('Next', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="fedistream-now-playing__progress">
|
||||
<div class="fedistream-now-playing__bar">
|
||||
<div class="fedistream-now-playing__bar-progress"></div>
|
||||
</div>
|
||||
<div class="fedistream-now-playing__times">
|
||||
<span class="fedistream-now-playing__time fedistream-now-playing__time--current">0:00</span>
|
||||
<span class="fedistream-now-playing__time fedistream-now-playing__time--total">0:00</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
32
templates/widgets/popular-tracks.twig
Normal file
32
templates/widgets/popular-tracks.twig
Normal file
@@ -0,0 +1,32 @@
|
||||
{# Popular Tracks Widget Template #}
|
||||
{% if posts is not empty %}
|
||||
<ol class="fedistream-widget__list fedistream-widget__list--tracks">
|
||||
{% for post in posts %}
|
||||
<li class="fedistream-widget__item" data-track-id="{{ post.id }}">
|
||||
<a href="{{ post.permalink }}" class="fedistream-widget__link">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-widget__image">
|
||||
{% else %}
|
||||
<span class="fedistream-widget__placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="fedistream-widget__info">
|
||||
<span class="fedistream-widget__title">{{ post.title }}</span>
|
||||
{% if post.artist %}
|
||||
<span class="fedistream-widget__artist">{{ post.artist }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if post.play_count %}
|
||||
<span class="fedistream-widget__plays">{{ post.play_count|number_format }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<button type="button" class="fedistream-widget__play" data-track-id="{{ post.id }}" aria-label="{{ __('Play', 'wp-fedistream') }}">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
{% else %}
|
||||
<p class="fedistream-widget__empty">{{ __('No tracks yet.', 'wp-fedistream') }}</p>
|
||||
{% endif %}
|
||||
29
templates/widgets/recent-releases.twig
Normal file
29
templates/widgets/recent-releases.twig
Normal file
@@ -0,0 +1,29 @@
|
||||
{# Recent Releases Widget Template #}
|
||||
{% if posts is not empty %}
|
||||
<ul class="fedistream-widget__list fedistream-widget__list--releases">
|
||||
{% for post in posts %}
|
||||
<li class="fedistream-widget__item">
|
||||
<a href="{{ post.permalink }}" class="fedistream-widget__link">
|
||||
{% if post.thumbnail %}
|
||||
<img src="{{ post.thumbnail }}" alt="{{ post.title|e('html_attr') }}" class="fedistream-widget__image">
|
||||
{% else %}
|
||||
<span class="fedistream-widget__placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 14.5c-2.49 0-4.5-2.01-4.5-4.5S9.51 7.5 12 7.5s4.5 2.01 4.5 4.5-2.01 4.5-4.5 4.5zm0-5.5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z"/></svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="fedistream-widget__info">
|
||||
<span class="fedistream-widget__title">{{ post.title }}</span>
|
||||
{% if post.artist %}
|
||||
<span class="fedistream-widget__artist">{{ post.artist }}</span>
|
||||
{% endif %}
|
||||
{% if post.release_date %}
|
||||
<span class="fedistream-widget__date">{{ post.release_date }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="fedistream-widget__empty">{{ __('No releases yet.', 'wp-fedistream') }}</p>
|
||||
{% endif %}
|
||||
18
uninstall.php
Normal file
18
uninstall.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Uninstall WP FediStream.
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
// If uninstall not called from WordPress, exit.
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load autoloader.
|
||||
$autoloader = __DIR__ . '/vendor/autoload.php';
|
||||
if ( file_exists( $autoloader ) ) {
|
||||
require_once $autoloader;
|
||||
\WP_FediStream\Installer::uninstall();
|
||||
}
|
||||
200
wp-fedistream.php
Normal file
200
wp-fedistream.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.1.0
|
||||
* Requires at least: 6.4
|
||||
* Requires PHP: 8.3
|
||||
* Author: Marco Graetsch
|
||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
* Text Domain: wp-fedistream
|
||||
* Domain Path: /languages
|
||||
*
|
||||
* @package WP_FediStream
|
||||
*/
|
||||
|
||||
// Prevent direct file access.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin version.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_FEDISTREAM_VERSION', '0.1.0' );
|
||||
|
||||
/**
|
||||
* Plugin file path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_FEDISTREAM_FILE', __FILE__ );
|
||||
|
||||
/**
|
||||
* Plugin directory path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_FEDISTREAM_PATH', plugin_dir_path( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Plugin directory URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_FEDISTREAM_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Plugin basename.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_FEDISTREAM_BASENAME', plugin_basename( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Minimum WordPress version required.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_FEDISTREAM_MIN_WP_VERSION', '6.4' );
|
||||
|
||||
/**
|
||||
* Minimum PHP version required.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_FEDISTREAM_MIN_PHP_VERSION', '8.3' );
|
||||
|
||||
/**
|
||||
* Check requirements and bootstrap the plugin.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_fedistream_init(): void {
|
||||
// Check PHP version.
|
||||
if ( version_compare( PHP_VERSION, WP_FEDISTREAM_MIN_PHP_VERSION, '<' ) ) {
|
||||
add_action( 'admin_notices', 'wp_fedistream_php_version_notice' );
|
||||
return;
|
||||
}
|
||||
|
||||
// Check WordPress version.
|
||||
if ( version_compare( get_bloginfo( 'version' ), WP_FEDISTREAM_MIN_WP_VERSION, '<' ) ) {
|
||||
add_action( 'admin_notices', 'wp_fedistream_wp_version_notice' );
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Composer autoloader exists.
|
||||
$autoloader = WP_FEDISTREAM_PATH . 'vendor/autoload.php';
|
||||
if ( ! file_exists( $autoloader ) ) {
|
||||
add_action( 'admin_notices', 'wp_fedistream_autoloader_notice' );
|
||||
return;
|
||||
}
|
||||
|
||||
require_once $autoloader;
|
||||
|
||||
// Initialize the plugin.
|
||||
\WP_FediStream\Plugin::get_instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display PHP version notice.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_fedistream_php_version_notice(): void {
|
||||
$message = sprintf(
|
||||
/* translators: 1: Required PHP version, 2: Current PHP version */
|
||||
__( 'WP FediStream requires PHP version %1$s or higher. You are running PHP %2$s.', 'wp-fedistream' ),
|
||||
WP_FEDISTREAM_MIN_PHP_VERSION,
|
||||
PHP_VERSION
|
||||
);
|
||||
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display WordPress version notice.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_fedistream_wp_version_notice(): void {
|
||||
$message = sprintf(
|
||||
/* translators: 1: Required WordPress version, 2: Current WordPress version */
|
||||
__( 'WP FediStream requires WordPress version %1$s or higher. You are running WordPress %2$s.', 'wp-fedistream' ),
|
||||
WP_FEDISTREAM_MIN_WP_VERSION,
|
||||
get_bloginfo( 'version' )
|
||||
);
|
||||
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display autoloader notice.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_fedistream_autoloader_notice(): void {
|
||||
$message = __( 'WP FediStream requires Composer dependencies to be installed. Please run "composer install" in the plugin directory.', 'wp-fedistream' );
|
||||
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin activation hook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_fedistream_activate(): void {
|
||||
// Check requirements before activation.
|
||||
if ( version_compare( PHP_VERSION, WP_FEDISTREAM_MIN_PHP_VERSION, '<' ) ) {
|
||||
deactivate_plugins( WP_FEDISTREAM_BASENAME );
|
||||
wp_die(
|
||||
sprintf(
|
||||
/* translators: %s: Required PHP version */
|
||||
esc_html__( 'WP FediStream requires PHP version %s or higher.', 'wp-fedistream' ),
|
||||
WP_FEDISTREAM_MIN_PHP_VERSION
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$autoloader = WP_FEDISTREAM_PATH . 'vendor/autoload.php';
|
||||
if ( file_exists( $autoloader ) ) {
|
||||
require_once $autoloader;
|
||||
\WP_FediStream\Installer::activate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin deactivation hook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_fedistream_deactivate(): void {
|
||||
$autoloader = WP_FEDISTREAM_PATH . 'vendor/autoload.php';
|
||||
if ( file_exists( $autoloader ) ) {
|
||||
require_once $autoloader;
|
||||
\WP_FediStream\Installer::deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin uninstall hook.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function wp_fedistream_uninstall(): void {
|
||||
$autoloader = WP_FEDISTREAM_PATH . 'vendor/autoload.php';
|
||||
if ( file_exists( $autoloader ) ) {
|
||||
require_once $autoloader;
|
||||
\WP_FediStream\Installer::uninstall();
|
||||
}
|
||||
}
|
||||
|
||||
// Register activation/deactivation hooks.
|
||||
register_activation_hook( __FILE__, 'wp_fedistream_activate' );
|
||||
register_deactivation_hook( __FILE__, 'wp_fedistream_deactivate' );
|
||||
|
||||
// Initialize plugin after all plugins are loaded.
|
||||
add_action( 'plugins_loaded', 'wp_fedistream_init' );
|
||||
Reference in New Issue
Block a user