diff --git a/.phpcs.xml b/.phpcs.xml
index d8870e5..eadd241 100644
--- a/.phpcs.xml
+++ b/.phpcs.xml
@@ -28,7 +28,7 @@
vendor/
-
+
diff --git a/README.md b/README.md
index d2a7bbc..b3626aa 100644
--- a/README.md
+++ b/README.md
@@ -4,24 +4,27 @@ Contributors: alleyinteractive
Tags: alleyinteractive, wp-video-sync
-Stable tag: 0.0.0
+Stable tag: 0.1.0
-Requires at least: 5.9
+Requires at least: 6.0
-Tested up to: 6.1
+Tested up to: 6.6
-Requires PHP: 8.1
+Requires PHP: 8.2
License: GPL v2 or later
-[![Coding Standards](https://github.com/alleyinteractive/wp-video-sync/actions/workflows/coding-standards.yml/badge.svg)](https://github.com/alleyinteractive/wp-video-sync/actions/workflows/coding-standards.yml)
-[![Testing Suite](https://github.com/alleyinteractive/wp-video-sync/actions/workflows/unit-test.yml/badge.svg)](https://github.com/alleyinteractive/wp-video-sync/actions/workflows/unit-test.yml)
+[![Testing Suite](https://github.com/alleyinteractive/wp-video-sync/actions/workflows/all-pr-tests.yml/badge.svg)](https://github.com/alleyinteractive/wp-video-sync/actions/workflows/all-pr-tests.yml)
Sync videos from a hosting provider to WordPress.
+Runs a scheduled task to sync videos from a supported video hosting provider to WordPress in batches based on the last modified date of the video. Implementers are responsible for installing and configuring a compatible plugin, choosing it as an adapter, and defining the callback that will be run for each video, which will be responsible for performing any post creations or updates in WordPress.
+
+This plugin is a great way to sync videos uploaded to a hosting provider (such as JW Player) to WordPress, such that the video itself remains on the hosting provider, but the video can be displayed in WordPress using a player block or shortcode, appears at its own unique URL, and can be included in search results.
+
## Installation
-You can install the package via composer:
+You can install the package via Composer:
```bash
composer require alleyinteractive/wp-video-sync
@@ -32,90 +35,72 @@ composer require alleyinteractive/wp-video-sync
Activate the plugin in WordPress and use it like so:
```php
-$plugin = Alley\WP\WP_Video_Sync\WP_Video_Sync\WP_Video_Sync();
-$plugin->perform_magic();
+use Alley\WP\WP_Video_Sync\Adapters\JW_Player_7_For_WP;
+use Alley\WP\WP_Video_Sync\Sync_Manager;
+use DateInterval;
+use DateTimeImmutable;
+use WP_Query;
+
+add_action( 'plugins_loaded', function () => {
+ $sync_manager = Sync_Manager::init()
+ ->with_adapter( new JW_Player_7_For_WP() )
+ ->with_frequency( 'hourly' )
+ ->with_batch_size( 1000 )
+ ->with_callback(
+ function ( $video ) {
+ $existing_video = new WP_Query( [ 'meta_key' => '_jwppp-video-url-1', 'meta_value' => $video->id ] );
+ $existing_id = $existing_video->posts[0]->ID ?? 0;
+ $duration = '';
+ try {
+ if ( ! empty( $video->metadata->duration ) ) {
+ $duration = ( new DateTimeImmutable() )
+ ->add( new DateInterval( sprintf( 'PT%dS', (int) $video->metadata->duration ) ) )
+ ->diff( new DateTimeImmutable() )->format( 'H:i:s' );
+ }
+ } catch ( Exception $e ) {
+ $duration = '';
+ }
+ wp_insert_post(
+ [
+ 'ID' => $existing_id,
+ 'post_type' => 'post',
+ 'post_status' => 'publish',
+ 'post_title' => $video->metadata->title,
+ 'post_content' => $video->metadata->description ?? '',
+ 'post_date' => DateTimeImmutable::createFromFormat( DATE_W3C, $video->created )->format( 'Y-m-d H:i:s' ),
+ 'post_modified' => DateTimeImmutable::createFromFormat( DATE_W3C, $video->last_modified )->format( 'Y-m-d H:i:s' ),
+ 'meta_input' => [
+ '_jwppp-video-url-1' => $video->id,
+ '_jwppp-cloud-playlist-1' => 'no',
+ '_jwppp-sources-number-1' => 1,
+ '_jwppp-video-title-1' => $video->metadata->title,
+ '_jwppp-video-description-1' => $video->metadata->description ?? '',
+ '_jwppp-activate-media-type-1' => 0,
+ '_jwppp-playlist-carousel-1' => 0,
+ '_jwppp-video-duration-1' => $duration,
+ '_jwppp-video-tags-1' => $video->metadata->tags ?? '',
+ ],
+ ]
+ );
+ }
+ );
+} );
```
-
-## Testing
-
-Run `npm run test` to run Jest tests against JavaScript files. Run
-`npm run test:watch` to keep the test runner open and watching for changes.
-
-Run `npm run lint` to run ESLint against all JavaScript files. Linting will also
-happen when running development or production builds.
-
-Run `composer test` to run tests against PHPUnit and the PHP code in the plugin.
-
-### The `entries` directory and entry points
-
-All directories created in the `entries` directory can serve as entry points and will be compiled with [@wordpress/scripts](https://github.com/WordPress/gutenberg/blob/trunk/packages/scripts/README.md#scripts) into the `build` directory with an accompanied `index.asset.php` asset map.
-
-#### Scaffolding an entry point
-To generate a new entry point, run the following command:
+This will configure the plugin to import a batch of 1000 videos every hour from JW Player, sorted by least to most recently updated, starting with the date and time of the last video that was updated. If videos have already been imported (as identified by the postmeta value saved for the unique video ID) they will be updated rather than created. New videos will be created. The example code above uses the `post` post type for this purpose, but the code could easily be adapted to use a custom post type. Additionally, the post content could be set to include a Gutenberg block or a shortcode for a player.
-```sh
-npm run create-entry
-```
-
-To generate a new slotfill, run the following command:
-
-```sh
-npm run create-slotfill
-```
-
-The command will prompt the user through several options for creating an entry or slotfill. The entries are scaffolded with the `@alleyinteractive/create-entry` script. Run the help command to see all the options:
-
-```sh
-npx @alleyinteractive/create-entry --help
-```
-[Visit the package README](https://www.npmjs.com/package/@alleyinteractive/create-entry) for more information.
+### Supported Adapters
-#### Enqueuing Entry Points
+As of now, the plugin only supports JW Player 7 for WordPress (both the free and premium versions). Other adapters may be added in the future.
-You can also include an `index.php` file in the entry point directory for enqueueing or registering a script. This file will then be moved to the build directory and will be auto-loaded with the `load_scripts()` function in the `functions.php` file. Alternatively, if a script is to be enqueued elsewhere there are helper functions in the `src/assets.php` file for getting the assets.
+#### JW Player 7 for WordPress
-### Scaffold a dynamic block with `create-block`
-
-Use the `create-block` command to create custom blocks with [@alleyinteractive/create-block](https://github.com/alleyinteractive/alley-scripts/tree/main/packages/create-block) script and follow the prompts to generate all the block assets in the `blocks/` directory.
-Block registration, script creation, etc will be scaffolded from the `create-block` script. Run `npm run build` to compile and build the custom block. Blocks are enqueued using the `load_scripts()` function in `src/assets.php`.
-
-### Updating WP Dependencies
-
-Update the [WordPress dependency packages](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#packages-update) used in the project to their latest version.
-
-To update `@wordpress` dependencies to their latest version use the packages-update command:
-
-```sh
-npx wp-scripts packages-update
-```
-
-This script provides the following custom options:
-
-- `--dist-tag` – allows specifying a custom dist-tag when updating npm packages. Defaults to `latest`. This is especially useful when using [`@wordpress/dependency-extraction-webpack-plugin`](https://www.npmjs.com/package/@wordpress/dependency-extraction-webpack-plugin). It lets installing the npm dependencies at versions used by the given WordPress major version for local testing, etc. Example:
-
-```sh
-npx wp-scripts packages-update --dist-tag=wp-WPVERSION`
-```
-
-Where `WPVERSION` is the version of WordPress you are targeting. The version
-must include both the major and minor version (e.g., `6.1`). For example:
-
-```sh
-npx wp-scripts packages-update --dist-tag=wp-6.1`
-```
+- Requires the [JW Player 7 for WordPress](https://wordpress.org/plugins/jw-player-7-for-wp/) plugin to be installed, activated, and properly configured with access credentials. Also supports the premium version.
+- The video object in the callback is a `stdClass` with the properties described in the `media` object under response code `200` in [the JW Player API documentation for the media list endpoint](https://docs.jwplayer.com/platform/reference/get_v2-sites-site-id-media).
## Releasing the Plugin
-The plugin uses a [built release workflow](./.github/workflows/built-release.yml)
-to compile and tag releases. Whenever a new version is detected in the root
-`composer.json` file or in the plugin's headers, the workflow will automatically
-build the plugin and tag it with a new version. The built tag will contain all
-the required front-end assets the plugin may require. This works well for
-publishing to WordPress.org or for submodule-ing.
-
-When you are ready to release a new version of the plugin, you can run
-`npm run release` to start the process of setting up a new release.
+New versions of this plugin will be created as releases in GitHub once ready.
## Changelog
@@ -132,4 +117,4 @@ with us](https://alley.co/careers/).
## License
-The GNU General Public License (GPL) license. Please see [License File](LICENSE) for more information.
\ No newline at end of file
+The GNU General Public License (GPL) license. Please see [License File](LICENSE) for more information.
diff --git a/src/adapters/class-jw-player-7-for-wp.php b/src/adapters/class-jw-player-7-for-wp.php
new file mode 100644
index 0000000..b3957e3
--- /dev/null
+++ b/src/adapters/class-jw-player-7-for-wp.php
@@ -0,0 +1,68 @@
+last_modified_date;
+ }
+
+ /**
+ * Fetches videos from JW Player that were modified after the provided DateTime.
+ *
+ * @param DateTimeImmutable $updated_after Return videos modified after this date.
+ * @param int $batch_size The number of videos to fetch in each batch.
+ *
+ * @return stdClass[] An array of video data.
+ */
+ public function get_videos( DateTimeImmutable $updated_after, int $batch_size ): array {
+ // Check if the JW Player 7 for WP plugin is active (free or premium).
+ if ( class_exists( 'JWPPP_Dashboard_Api' ) ) {
+ $api = new \JWPPP_Dashboard_Api();
+ $result = $api->call(
+ sprintf(
+ 'media/?q=last_modified:[%s TO *]&page=1&page_length=%d&sort=last_modified:asc',
+ $updated_after->format( 'Y-m-d' ),
+ $batch_size
+ )
+ );
+ $videos = $result->media ?? [];
+
+ // Attempt to set the last modified date.
+ if ( isset( $videos[ count( $videos ) - 1 ]->last_modified ) ) {
+ $last_modified_date = DateTimeImmutable::createFromFormat( DATE_W3C, $videos[ count( $videos ) - 1 ]->last_modified );
+ if ( $last_modified_date instanceof DateTimeImmutable ) {
+ $this->last_modified_date = $last_modified_date;
+ }
+ }
+
+ return $videos;
+ }
+
+ return [];
+ }
+}
diff --git a/src/autoload.php b/src/autoload.php
new file mode 100644
index 0000000..7516215
--- /dev/null
+++ b/src/autoload.php
@@ -0,0 +1,34 @@
+frequency, self::CRON_HOOK );
+ }
+ }
+
+ /**
+ * Syncs videos from the provider.
+ *
+ * @throws Error If unable to parse the last sync as a DateTimeImmutable object.
+ */
+ public function sync_videos(): void {
+ define( 'WP_IMPORTING', true ); // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
+
+ // If there isn't a valid adapter, bail.
+ if ( ! $this->adapter instanceof Adapter ) {
+ throw new Error( esc_html__( 'WP Video Sync: Unable to sync videos without a valid adapter.', 'wp-video-sync' ) );
+ }
+
+ // If there isn't a valid callback, bail.
+ if ( ! is_callable( $this->callback ) ) {
+ throw new Error( esc_html__( 'WP Video Sync: Unable to execute provided callback.', 'wp-video-sync' ) );
+ }
+
+ // Try to get the option value so we can parse it as a date. If it doesn't exist, default to the Unix epoch.
+ $option_value = get_option( self::LAST_SYNC_OPTION, '1970-01-01T00:00:00Z' );
+ if ( ! is_string( $option_value ) ) {
+ throw new Error( esc_html__( 'WP Video Sync: The value saved to the options table for the last sync time is not a string.', 'wp-video-sync' ) );
+ }
+
+ // Try to parse the last sync time into a DateTimeImmutable object and fail if we can't.
+ $last_sync = DateTimeImmutable::createFromFormat( DATE_W3C, $option_value );
+ if ( ! $last_sync instanceof DateTimeImmutable ) {
+ throw new Error( esc_html__( 'WP Video Sync: The last sync time could not be parsed into a DateTimeImmutable object.', 'wp-video-sync' ) );
+ }
+
+ // Get a batch of videos and loop over them and process each.
+ $videos = $this->adapter->get_videos( $last_sync, $this->batch_size );
+ foreach ( $videos as $video ) {
+ call_user_func_array( $this->callback, [ $video ] );
+ }
+
+ // Try to update the last sync time to the last modified time of the last video that was processed.
+ $next_last_modified = $this->adapter->get_last_modified_date();
+ if ( $next_last_modified instanceof DateTimeImmutable ) {
+ update_option( self::LAST_SYNC_OPTION, $next_last_modified->format( DATE_W3C ), false );
+ }
+ }
+
+ /**
+ * Allows the adapter to be set. Can be used with an adapter that ships with this plugin or any custom adapter that implements the Adapter interface.
+ *
+ * @param Adapter $adapter The adapter to load.
+ *
+ * @return $this For chaining configuration.
+ */
+ public function with_adapter( Adapter $adapter ): self {
+ $this->adapter = $adapter;
+ return $this;
+ }
+
+ /**
+ * Allows the batch size to be configured.
+ *
+ * @param int $batch_size The number of videos to process in each batch.
+ *
+ * @return $this For chaining configuration.
+ */
+ public function with_batch_size( int $batch_size ): self {
+ $this->batch_size = $batch_size;
+ return $this;
+ }
+
+ /**
+ * Allows a callback to be set that will be called when the sync runs.
+ *
+ * @param callable $callback The callback to run when the sync runs.
+ *
+ * @return $this For chaining configuration.
+ */
+ public function with_callback( callable $callback ): self {
+ $this->callback = $callback;
+ return $this;
+ }
+
+ /**
+ * Allows the sync frequency to be set. Can be any valid value for wp_schedule_event().
+ *
+ * @param string $frequency The frequency with which to sync videos.
+ *
+ * @return $this For chaining configuration.
+ */
+ public function with_frequency( string $frequency ): self {
+ $this->frequency = $frequency;
+ return $this;
+ }
+}
diff --git a/src/class-wp-video-sync.php b/src/class-wp-video-sync.php
deleted file mode 100644
index 8d3dd64..0000000
--- a/src/class-wp-video-sync.php
+++ /dev/null
@@ -1,15 +0,0 @@
-assertTrue( true );
- $this->assertNotEmpty( home_url() );
- }
-}
diff --git a/tests/Feature/JWPlayerAdapterTest.php b/tests/Feature/JWPlayerAdapterTest.php
new file mode 100644
index 0000000..bf0ea56
--- /dev/null
+++ b/tests/Feature/JWPlayerAdapterTest.php
@@ -0,0 +1,68 @@
+fake_request( 'https://api.jwplayer.com/v2/sites/example-api-key/media/*' )
+ ->with_response_code( 200 )
+ ->with_body( file_get_contents( __DIR__ . '/../Fixtures/jw-player-api-v2-media.json' ) );
+
+ // Create an instance of the adapter.
+ $sync_manager = Sync_Manager::init()
+ ->with_adapter( new JW_Player_7_For_WP() )
+ ->with_callback( fn ( $video ) => self::factory()->post->create(
+ [
+ 'post_title' => $video->metadata->title,
+ 'post_date' => DateTimeImmutable::createFromFormat( DATE_W3C, $video->created )->format( 'Y-m-d H:i:s' ),
+ 'post_modified' => DateTimeImmutable::createFromFormat( DATE_W3C, $video->last_modified )->format( 'Y-m-d H:i:s' ),
+ 'meta_input' => [
+ 'jwplayer_id' => $video->id,
+ ],
+ ]
+ ) );
+
+ // Run the sync.
+ $sync_manager->sync_videos();
+
+ // Confirm that the sync was successful.
+ $video_query = new WP_Query(
+ [
+ 'name' => 'example-video',
+ 'post_status' => 'publish',
+ 'post_type' => 'post',
+ ]
+ );
+ $this->assertEquals( 1, $video_query->post_count );
+ $this->assertEquals( 'Example Video', $video_query->posts[0]->post_title );
+ $this->assertEquals( '2024-01-01 12:00:00', $video_query->posts[0]->post_date );
+ $this->assertEquals( '2024-01-01 12:00:00', $video_query->posts[0]->post_date_gmt );
+ $this->assertEquals( '2024-01-01 13:00:00', $video_query->posts[0]->post_modified );
+ $this->assertEquals( '2024-01-01 13:00:00', $video_query->posts[0]->post_modified_gmt );
+ $this->assertEquals( 'ABCD1234', get_post_meta( $video_query->posts[0]->ID, 'jwplayer_id', true ) );
+
+ // Ensure that the sync time was updated.
+ $this->assertEquals( '2024-01-01T13:00:00+00:00', get_option( Sync_Manager::LAST_SYNC_OPTION ) );
+ }
+}
diff --git a/tests/Fixtures/jw-player-api-v2-media.json b/tests/Fixtures/jw-player-api-v2-media.json
new file mode 100644
index 0000000..c7971e9
--- /dev/null
+++ b/tests/Fixtures/jw-player-api-v2-media.json
@@ -0,0 +1,36 @@
+{
+ "media": [
+ {
+ "created": "2024-01-01T12:00:00+00:00",
+ "duration": 1234.56789,
+ "error_message": null,
+ "external_id": null,
+ "hosting_type": "hosted",
+ "id": "ABCD1234",
+ "last_modified": "2024-01-01T13:00:00+00:00",
+ "media_type": "video",
+ "metadata": {
+ "author": null,
+ "category": null,
+ "custom_params": {},
+ "description": null,
+ "external_id": null,
+ "language": null,
+ "permalink": null,
+ "protection_rule_key": null,
+ "publish_end_date": null,
+ "publish_start_date": "2024-01-01T12:00:00+00:00",
+ "tags": [],
+ "title": "Example Video"
+ },
+ "mime_type": null,
+ "relationships": {},
+ "schema": null,
+ "source_url": null,
+ "status": "ready",
+ "trim_in_point": null,
+ "trim_out_point": null,
+ "type": "media"
+ }
+ ]
+}
diff --git a/tests/Mocks/JWPPP_Dashboard_API.php b/tests/Mocks/JWPPP_Dashboard_API.php
new file mode 100644
index 0000000..a797a44
--- /dev/null
+++ b/tests/Mocks/JWPPP_Dashboard_API.php
@@ -0,0 +1,25 @@
+assertTrue( true );
- }
-}
diff --git a/tests/Unit/SyncManagerTest.php b/tests/Unit/SyncManagerTest.php
new file mode 100644
index 0000000..23250bc
--- /dev/null
+++ b/tests/Unit/SyncManagerTest.php
@@ -0,0 +1,28 @@
+with_adapter( new JW_Player_7_For_WP() )
+ ->with_frequency( 'daily' );
+ $this->assertInstanceOf( JW_Player_7_For_WP::class, $sync_manager->adapter );
+ $this->assertEquals( 'daily', $sync_manager->frequency );
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 9fc102a..438346d 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -2,14 +2,12 @@
/**
* WP Video Sync Tests: Bootstrap
*
- * phpcs:disable Squiz.Commenting.InlineComment.InvalidEndChar
- *
* @package wp-video-sync
*/
-/**
- * Visit {@see https://mantle.alley.com/testing/test-framework.html} to learn more.
- */
+// Ensure Composer autoloader is loaded.
+require_once __DIR__ . '/../vendor/autoload.php';
+
\Mantle\Testing\manager()
// Rsync the plugin to plugins/wp-video-sync when testing.
->maybe_rsync_plugin()
diff --git a/wp-video-sync.php b/wp-video-sync.php
index 8a3ab31..77d17f3 100644
--- a/wp-video-sync.php
+++ b/wp-video-sync.php
@@ -3,10 +3,10 @@
* Plugin Name: WP Video Sync
* Plugin URI: https://github.com/alleyinteractive/wp-video-sync
* Description: Sync videos from a hosting provider to WordPress
- * Version: 0.0.0
+ * Version: 0.1.0
* Author: Alley
* Author URI: https://github.com/alleyinteractive/wp-video-sync
- * Requires at least: 6.6
+ * Requires at least: 6.0
* Tested up to: 6.6
*
* Text Domain: wp-video-sync
@@ -21,4 +21,4 @@
exit;
}
-require_once __DIR__ . '/src/class-wp-video-sync.php';
+require_once __DIR__ . '/src/autoload.php';