diff --git a/CHANGELOG.md b/CHANGELOG.md index 2268bb0..cd0187f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +### 6.1.0 pl - *12. May 2026* + * Added SRF / SRG SSR adapter (Swiss Radio and Television) + - Supports share URLs with `?urn=` query parameter + - Supports `/redirect/detail/UUID` path format + - Supports plain URNs (`urn:srf:video:UUID`) and plain UUIDs + - Sister channels: RTS, RTR, RSI, SWI — detected by domain + - Thumbnail fetched from SRG SSR Integration Layer API + - SRF listed first in the default `processors` pipeline + * Adopted adapter architecture from upstream Videobox-MODX (HitkoDev) + * Removed hardcoded `$_GET['dev']` debug paths from `loadAssets()` + * Added graceful fallback when `overrides.min.css` is missing + * Build script updated for CLI execution (pre-defines URL constants) + * Default player dimensions changed to 960×540 (16:9) + * Added README.md and CHANGELOG.md to transport package attributes + ### 6.0.0 rc 7 - *20. December 2016* * customise nothumb colour * fix HTML5 player template @@ -20,7 +35,7 @@ ### 6.0.0 rc 3 - *19. November 2016* * fix bug with missing / inaccessible thumbnails - * fix bug with loading Videobox in `````` + * fix bug with loading Videobox in `` * add missing thumbnail images ### 6.0.0 rc 2 - *14. November 2016* @@ -47,42 +62,12 @@ * reduce animation flickering * update dependencies * recompile CSS with better backwards compatibility - * update the README file - -### 5.0.0 rc 5 - *8. May 2016* - * fix bug with PHP-GD - * update dependencies - -### 5.0.0 rc 4 - *28. April 2016* - * fix slider bug - * allow users to modify the UI colour - -### 5.0.0 rc 3 - *29. March 2016* - * updated core - * bug fixes - -### 5.0.0 rc 2 - *22. March 2016* - * code fixes and optimisations ### 5.0.0 rc 1 - *14. March 2016* * added Twitch adapter -### 5.0.0 beta 4 - *7. March 2016* - * moved to TS and SASS - * bug fixes - * updated HTML5 player - * added root option for Videobox player (pop-up is now appended to options.root, which should be the default scroll wrap) - * improved overall performance - ### 5.0.0 beta 3 - *4. February 2016* - * js optimisation - * bug fixes * HTML5 player -### 5.0.0 beta 2 - *17. December 2015* - * moved from CSS transitions to Web Animations for better performance - * bug fixes - ### 5.0.0 beta 1 - *28. October 2015* * initial release - diff --git a/README.md b/README.md index 6d75ec2..3358f07 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,123 @@ -videobox logo -#[Videobox - a lightweight video player and gallery](https://hitko.eu/videobox) -Videobox is a MODX extension for adding video and audio to your site. It supports videos from YouTube and Vimeo, as well as music from SoundCloud, Twitch videos and streams, and HTML5 supported video and audio formats. There are many ways to insert a video player in the content, including the standard video player, a pop-up player, expanding inline player, or a gallery of videos. All you have to do is put ```[[Videobox? &videos=`XfSfK8AkGVo`]]``` and you get a beautiful responsive video player. - -###Demo -If you want to see Videobox in action, check out the [demo page](https://hitko.eu/videobox) - -###Basic options -The example above uses a single YouTube video ID, but Videobox can do much more. Below is an example of the full video syntax, including start and end time, video title, and multiple videos. Check out the [Getting started guide](https://hitko.eu/videobox/getting-started/#htk-videobox-for-modx) for a detailed explanation of the syntax, and [Videobox documentation](https://hitko.eu/videobox/documentation/) for a guide on the parameters you can use. - -``` -m--HWCQq5Ew#0:30-1:50|Alestorm ft. Lady Gaga - Magnetic Telephone|, -cTtka7r62V8|Soilwork - The Ride Majestic|, -mIegk9Ukx4k|Kontrust - Just Propaganda|, -FANEejDEvCA|Justin Hayward - The Story In Your Eyes|, -4auH5kwmd_Y|Kataklysm - The Black Sheep -``` \ No newline at end of file +# Videobox MODX Extra + +A MODX Revolution Extra that wraps the [Videobox JS library](https://github.com/HitkoDev/Videobox) and provides a flexible video embedding system with server-side thumbnail generation, multiple display modes, and an extensible adapter architecture. + +## Features + +- **SRF / SRG SSR** — Swiss public broadcasting (SRF, RTS, RTR, RSI, SWI) +- **YouTube** — watch URLs, short links (youtu.be), plain video IDs +- **Vimeo** — video URLs and plain IDs +- **SoundCloud** — track URLs +- **Twitch** — channel and VOD URLs +- **HTML5 / local files** — with automatic format conversion via FFmpeg +- **Server-side thumbnail generation** — GD or Imagick, with caching +- **Display modes** — inline player, lightbox, box, thumbnail gallery, slider, text links +- **Pagination** for large galleries +- **Color theming** — configurable accent colours applied via CSS overrides +- **MODX cache integration** + +## Requirements + +- MODX Revolution 2.6+ or MODX 3.x +- PHP 7.4+ +- GD or Imagick extension (for thumbnail generation) + +## Installation + +**Option A — Package Manager** +1. Copy `videobox-6.1.0-pl.transport.zip` into `core/packages/` on your MODX site. +2. Manager → Extras → Installer → Search Locally for Packages. +3. Install **Videobox 6.1.0**. + +**Option B — Build from source** +```bash +cp _build/build.config.sample.php _build/build.config.php +# edit _build/build.config.php to match your MODX installation +php _build/build.transport.php +``` + +## Basic usage + +``` +[[Videobox? &videos=`https://www.srf.ch/play/tv/-/video/-?urn=urn:srf:video:608cc6ac-2262-4bfb-98ce-d04f58df5438|Tagesschau`]] +[[Videobox? &videos=`https://www.srf.ch/play/tv/redirect/detail/acedb774-8d9c-43f1-9a12-26a9a31083ff|My SRF Video`]] +[[Videobox? &videos=`https://www.youtube.com/watch?v=dQw4w9WgXcQ|Never Gonna Give You Up`]] +[[Videobox? &videos=`KKWTdo5YW_I|YouTube by ID`]] +[[Videobox? &videos=`https://vimeo.com/123456789|Vimeo video`]] +``` + +### Multiple videos (gallery) + +``` +[[Videobox? + &videos=`urn:srf:video:608cc6ac-2262-4bfb-98ce-d04f58df5438|Tagesschau|, + https://www.youtube.com/watch?v=dQw4w9WgXcQ|YouTube clip` + &display=`gallery` + &tWidth=`480` + &tHeight=`270` +]] +``` + +## Video input format + +The `&videos` parameter accepts a pipe-delimited list of entries separated by `|,`: + +``` +ID_or_URL|Optional Title#start-end|,ID_or_URL|Title +``` + +- `|` separates ID/URL from the title +- `#start` / `#start-end` add time offsets (e.g. `#1:30-2:45`) +- `|,` separates multiple videos in one call + +## Snippet parameters + +| Parameter | Default | Description | +|---|---|---| +| `videos` / `video` | — | Video IDs, URLs, or pipe-delimited list | +| `display` | auto | `player` · `box` · `links` · `gallery` · `slider` | +| `player` | `videobox` | `videobox` (lightbox) · `vbinline` (inline) | +| `singleDisplay` | `player` | Default display for a single video | +| `multipleDisplay` | `gallery` | Default display for multiple videos | +| `pWidth` | `960` | Player width in px | +| `pHeight` | `540` | Player height in px | +| `tWidth` | `480` | Thumbnail width in px | +| `tHeight` | `360` | Thumbnail height in px | +| `perPage` | `12` | Gallery items per page (0 = all) | +| `processors` | `SRF, Twitch, YouTube, Vimeo, SoundCloud, HTML5` | Adapter pipeline | +| `color` | `50bf82` | Player accent colour (hex) | +| `tColor` | `005723` | UI text colour (hex) | +| `hColor` | `50bf82` | UI hover colour (hex) | +| `bgColor` | `00a645` | Placeholder thumbnail colour (hex) | +| `cache` | `true` | Enable output and thumbnail caching | +| `boxTpl` | `vb.boxTpl` | Chunk for box display | +| `galleryTpl` | `vb.galleryTpl` | Chunk for gallery wrapper | +| `galleryItemTpl` | `vb.galleryItemTpl` | Chunk for gallery item | +| `thumbTpl` | `vb.thumbTpl` | Chunk for thumbnail link | +| `linkTpl` | `vb.linkTpl` | Chunk for text link | +| `playerTpl` | `vb.playerTpl` | Chunk for inline player | +| `sliderTpl` | `vb.sliderTpl` | Chunk for slider wrapper | +| `sliderItemTpl` | `vb.galleryItemTpl` | Chunk for slider item | + +## Supported SRF URL formats + +| Input | Resolved embed URL | +|---|---| +| `https://www.srf.ch/play/tv/-/video/-?urn=urn:srf:video:UUID` | `https://www.srf.ch/play/embed?urn=urn:srf:video:UUID` | +| `https://www.srf.ch/play/tv/redirect/detail/UUID` | `https://www.srf.ch/play/embed?urn=urn:srf:video:UUID` | +| `urn:srf:video:UUID` | `https://www.srf.ch/play/embed?urn=urn:srf:video:UUID` | +| `UUID` (plain) | `https://www.srf.ch/play/embed?urn=urn:srf:video:UUID` | + +SRG SSR sister channels (RTS, RTR, RSI, SWI) follow the same patterns on their respective domains. + +## Adapters + +Each platform is implemented as a pair of files: +- `core/components/videobox/model/adapters/{name}.class.php` — the adapter class +- `core/components/videobox/elements/snippets/snippet.{name}.php` — the MODX processor snippet + +To add a custom adapter, create both files and add the snippet name to the `&processors` parameter. + +## License + +GPL-3.0 — same as the upstream [Videobox JS library](https://github.com/HitkoDev/Videobox). diff --git a/_build/build.transport.php b/_build/build.transport.php index 58eb096..65c1eae 100644 --- a/_build/build.transport.php +++ b/_build/build.transport.php @@ -1,47 +1,55 @@ $root, - 'build' => $root . '_build/', - 'data' => $root . '_build/data/', - 'resolvers' => $root . '_build/resolvers/', - 'chunks' => $root.'core/components/'.PKG_NAME_LOWER.'/chunks/', - 'lexicon' => $root . 'core/components/'.PKG_NAME_LOWER.'/lexicon/', - 'docs' => $root.'core/components/'.PKG_NAME_LOWER.'/docs/', - 'elements' => $root.'core/components/'.PKG_NAME_LOWER.'/elements/', - 'source_assets' => $root.'assets/components/'.PKG_NAME_LOWER, - 'source_core' => $root.'core/components/'.PKG_NAME_LOWER, + 'root' => $root, + 'build' => $root . '_build/', + 'data' => $root . '_build/data/', + 'resolvers' => $root . '_build/resolvers/', + 'elements' => $root . 'core/components/' . PKG_NAME_LOWER . '/elements/', + 'source_assets' => $root . 'assets/components/' . PKG_NAME_LOWER, + 'source_core' => $root . 'core/components/' . PKG_NAME_LOWER, ); -unset($root); - -/* override with your own defines here (see build.config.sample.php) */ -require_once $sources['build'] . 'build.config.php'; + +/* bootstrap MODX */ require_once MODX_CORE_PATH . 'model/modx/modx.class.php'; - -$modx= new modX(); + +$modx = new modX(); $modx->initialize('mgr'); -echo '
'; /* used for nice formatting of log messages */
 $modx->setLogLevel(modX::LOG_LEVEL_INFO);
-$modx->setLogTarget('ECHO');
- 
-$modx->loadClass('transport.modPackageBuilder','',false, true);
+$modx->setLogTarget(XPDO_CLI_MODE ? 'ECHO' : 'HTML');
+
+$modx->loadClass('transport.modPackageBuilder', '', false, true);
 $builder = new modPackageBuilder($modx);
-$builder->createPackage(PKG_NAME,PKG_VERSION,PKG_RELEASE);
-$builder->registerNamespace(PKG_NAME_LOWER,false,true,'{core_path}components/'.PKG_NAME_LOWER.'/');
+$builder->createPackage(PKG_NAME_LOWER, PKG_VERSION, PKG_RELEASE);
+$builder->registerNamespace(PKG_NAME_LOWER, false, true, '{core_path}components/' . PKG_NAME_LOWER . '/');
 
+// ── Categories ───────────────────────────────────────────────────────────────
 $category = $modx->newObject('modCategory');
 $category->set('category', PKG_NAME);
 
@@ -49,125 +57,96 @@
 $adaptersCategory->set('category', 'Adapters');
 $adaptersCategory->addOne($category);
 
-/* add snippets */
-$modx->log(modX::LOG_LEVEL_INFO,'Packaging in snippets...');
-$snippets = include $sources['data'].'transport.snippets.php';
-if (empty($snippets)) $modx->log(modX::LOG_LEVEL_ERROR,'Could not package in snippets.');
+// ── Main snippet ─────────────────────────────────────────────────────────────
+$modx->log(modX::LOG_LEVEL_INFO, 'Packaging main snippet...');
+$snippets = include $sources['data'] . 'transport.snippets.php';
+if (empty($snippets)) $modx->log(modX::LOG_LEVEL_ERROR, 'Could not package snippets.');
 $category->addMany($snippets);
 
-/* add adapter snippets */
-$modx->log(modX::LOG_LEVEL_INFO,'Packaging in adapter snippets...');
-$snippets = include $sources['data'].'transport.snippets.adapters.php';
-if (empty($snippets)) $modx->log(modX::LOG_LEVEL_ERROR,'Could not package in adapter snippets.');
+// ── Adapter snippets ─────────────────────────────────────────────────────────
+$modx->log(modX::LOG_LEVEL_INFO, 'Packaging adapter snippets (SRF, YouTube, Vimeo, SoundCloud, HTML5, Twitch)...');
+$snippets = include $sources['data'] . 'transport.snippets.adapters.php';
+if (empty($snippets)) $modx->log(modX::LOG_LEVEL_ERROR, 'Could not package adapter snippets.');
 $adaptersCategory->addMany($snippets);
 
-/* add adapter plugins */
-$modx->log(modX::LOG_LEVEL_INFO,'Packaging in adapter plugins...');
-$plugins = include $sources['data'].'transport.plugins.adapters.php';
-if (empty($plugins)) $modx->log(modX::LOG_LEVEL_ERROR,'Could not package in adapter plugins.');
+// ── Adapter plugins ──────────────────────────────────────────────────────────
+$modx->log(modX::LOG_LEVEL_INFO, 'Packaging adapter plugins...');
+$plugins = include $sources['data'] . 'transport.plugins.adapters.php';
+if (empty($plugins)) $modx->log(modX::LOG_LEVEL_ERROR, 'Could not package adapter plugins.');
 $adaptersCategory->addMany($plugins);
- 
-/* add chunks */
-$modx->log(modX::LOG_LEVEL_INFO,'Packaging in chunks...');
-$chunks = include $sources['data'].'transport.chunks.php';
-if (empty($chunks)) $modx->log(modX::LOG_LEVEL_ERROR,'Could not package in chunks.');
-$category->addMany($chunks);
- 
-/* copy files */
-
-// Videobox-JS
-copy($sources['root'] . 'node_modules/videobox/dist/videobox.min.css', $sources['root'] . 'assets/components/videobox/css/videobox.min.css');
-copy($sources['root'] . 'node_modules/videobox/dist/videobox.css.map', $sources['root'] . 'assets/components/videobox/css/videobox.css.map');
-copy($sources['root'] . 'node_modules/videobox/dist/overrides.min.css', $sources['root'] . 'assets/components/videobox/css/overrides.min.css');
-copy($sources['root'] . 'node_modules/videobox/dist/overrides.css.map', $sources['root'] . 'assets/components/videobox/css/overrides.css.map');
-copy($sources['root'] . 'node_modules/videobox/dist/videobox.bundle.js', $sources['root'] . 'assets/components/videobox/js/videobox.bundle.js');
-copy($sources['root'] . 'node_modules/videobox/dist/videobox.bundle.map', $sources['root'] . 'assets/components/videobox/js/videobox.bundle.map');
-
-copy($sources['root'] . 'node_modules/videobox/dist/nobg_audio.png', $sources['root'] . 'assets/components/videobox/img/nobg_audio.png');
-copy($sources['root'] . 'node_modules/videobox/dist/nobg_video.png', $sources['root'] . 'assets/components/videobox/img/nobg_video.png');
 
-// VideoJS
-copy($sources['root'] . 'node_modules/video.js/dist/video.min.js', $sources['root'] . 'assets/components/videobox/video-js/video.min.js');
+// ── Chunks ───────────────────────────────────────────────────────────────────
+$modx->log(modX::LOG_LEVEL_INFO, 'Packaging chunks...');
+$chunks = include $sources['data'] . 'transport.chunks.php';
+if (empty($chunks)) $modx->log(modX::LOG_LEVEL_ERROR, 'Could not package chunks.');
+$category->addMany($chunks);
 
-/* create category vehicle */
+// ── Vehicle (files + PHP resolver) ───────────────────────────────────────────
 $vehicle = $builder->createVehicle($adaptersCategory, array(
-    xPDOTransport::UNIQUE_KEY => 'category',
+    xPDOTransport::UNIQUE_KEY    => 'category',
     xPDOTransport::PRESERVE_KEYS => false,
     xPDOTransport::UPDATE_OBJECT => true,
     xPDOTransport::RELATED_OBJECTS => true,
-    xPDOTransport::RELATED_OBJECT_ATTRIBUTES => array (
-		'Plugins' => array (
-			xPDOTransport::PRESERVE_KEYS => false,
-			xPDOTransport::UPDATE_OBJECT => BUILD_PLUGIN_UPDATE,
-			xPDOTransport::UNIQUE_KEY => 'name',
-		),
+    xPDOTransport::RELATED_OBJECT_ATTRIBUTES => array(
+        'Plugins' => array(
+            xPDOTransport::PRESERVE_KEYS => false,
+            xPDOTransport::UPDATE_OBJECT => true,
+            xPDOTransport::UNIQUE_KEY    => 'name',
+        ),
         'Snippets' => array(
             xPDOTransport::PRESERVE_KEYS => false,
             xPDOTransport::UPDATE_OBJECT => true,
-            xPDOTransport::UNIQUE_KEY => 'name',
+            xPDOTransport::UNIQUE_KEY    => 'name',
+        ),
+        'Parent' => array(
+            xPDOTransport::PRESERVE_KEYS => false,
+            xPDOTransport::UPDATE_OBJECT => true,
+            xPDOTransport::UNIQUE_KEY    => 'category',
+            xPDOTransport::RELATED_OBJECTS => true,
+            xPDOTransport::RELATED_OBJECT_ATTRIBUTES => array(
+                'Snippets' => array(
+                    xPDOTransport::PRESERVE_KEYS => false,
+                    xPDOTransport::UPDATE_OBJECT => true,
+                    xPDOTransport::UNIQUE_KEY    => 'name',
+                ),
+                'Chunks' => array(
+                    xPDOTransport::PRESERVE_KEYS => false,
+                    xPDOTransport::UPDATE_OBJECT => true,
+                    xPDOTransport::UNIQUE_KEY    => 'name',
+                ),
+            ),
         ),
-		'Chunks' => array (
-			xPDOTransport::PRESERVE_KEYS => false,
-			xPDOTransport::UPDATE_OBJECT => true,
-			xPDOTransport::UNIQUE_KEY => 'name',
-		),
-		'Parent' => array (
-			xPDOTransport::PRESERVE_KEYS => false,
-			xPDOTransport::UPDATE_OBJECT => true,
-			xPDOTransport::UNIQUE_KEY => 'category',
-			xPDOTransport::RELATED_OBJECTS => true,
-			xPDOTransport::RELATED_OBJECT_ATTRIBUTES => array (
-				'Snippets' => array(
-					xPDOTransport::PRESERVE_KEYS => false,
-					xPDOTransport::UPDATE_OBJECT => true,
-					xPDOTransport::UNIQUE_KEY => 'name',
-				),
-				'Chunks' => array (
-					xPDOTransport::PRESERVE_KEYS => false,
-					xPDOTransport::UPDATE_OBJECT => true,
-					xPDOTransport::UNIQUE_KEY => 'name',
-				),
-			),
-		),
     ),
 ));
-$modx->log(modX::LOG_LEVEL_INFO,'Adding file resolvers to category...');
-$vehicle->resolve('file',array(
+
+$modx->log(modX::LOG_LEVEL_INFO, 'Adding file resolvers...');
+$vehicle->resolve('file', array(
     'source' => $sources['source_assets'],
     'target' => "return MODX_ASSETS_PATH . 'components/';",
-	xPDOTransport::FILE_RESOLVE_OPTIONS => array(
-		'copy_exclude_patterns' => array('/cache/', '/\.zip/', '/(?resolve('file',array(
+$vehicle->resolve('file', array(
     'source' => $sources['source_core'],
     'target' => "return MODX_CORE_PATH . 'components/';",
 ));
 
-$modx->log(modX::LOG_LEVEL_INFO,'Adding in PHP resolvers...');
-$vehicle->resolve('php',array(
+$modx->log(modX::LOG_LEVEL_INFO, 'Adding PHP resolver...');
+$vehicle->resolve('php', array(
     'source' => $sources['resolvers'] . 'resolve.events.php',
 ));
 
 $builder->putVehicle($vehicle);
- 
-/* zip up package */
-$modx->log(modX::LOG_LEVEL_INFO,'Packing up transport package zip...');
-$modx->log(modX::LOG_LEVEL_INFO,'Adding package attributes and setup options...');
+
+// ── Package attributes ───────────────────────────────────────────────────────
 $builder->setPackageAttributes(array(
-	'name' => PKG_NAME,
-	'author' => 'HitkoDev', 
-    'license' => file_get_contents($sources['root'] . 'LICENSE.md'),
-    'readme' => file_get_contents($sources['root'] . 'README.md'),
+    'name'      => PKG_NAME,
+    'author'    => 'HitkoDev / Pixmill',
+    'license'   => file_get_contents($sources['root'] . 'LICENSE.md'),
+    'readme'    => file_get_contents($sources['root'] . 'README.md'),
     'changelog' => file_get_contents($sources['root'] . 'CHANGELOG.md'),
 ));
+
+$modx->log(modX::LOG_LEVEL_INFO, 'Packing transport ZIP...');
 $builder->pack();
- 
-$tend= explode(" ", microtime());
-$tend= $tend[1] + $tend[0];
-$totalTime= sprintf("%2.4f s",($tend - $tstart));
-$modx->log(modX::LOG_LEVEL_INFO,"\n
Package Built.
\nExecution time: {$totalTime}\n"); - - -session_write_close(); -exit (); + +$total = sprintf('%2.4f s', microtime(true) - $tstart); +$modx->log(modX::LOG_LEVEL_INFO, "Package built in {$total}"); diff --git a/_build/data/properties/properties.videobox.php b/_build/data/properties/properties.videobox.php index 47af466..94e15da 100644 --- a/_build/data/properties/properties.videobox.php +++ b/_build/data/properties/properties.videobox.php @@ -1,3 +1,27 @@ newObject('modSnippet'); -$snippet->fromArray(array( - 'name' => 'YouTube', - 'description' => 'YouTube adapter', - 'snippet' => getSnippetContent($sources['elements'].'snippets/snippet.youtube.php'), -), '', true, true); -$snippets[] = $snippet; +$defs = array( + array('name' => 'SRF', 'desc' => 'SRF / SRG SSR adapter (Swiss public broadcasting)', 'file' => 'snippet.srf.php', 'props' => null), + array('name' => 'YouTube', 'desc' => 'YouTube adapter', 'file' => 'snippet.youtube.php', 'props' => null), + array('name' => 'Vimeo', 'desc' => 'Vimeo adapter', 'file' => 'snippet.vimeo.php', 'props' => null), + array('name' => 'SoundCloud', 'desc' => 'SoundCloud adapter', 'file' => 'snippet.soundcloud.php', 'props' => 'properties.soundcloud.php'), + array('name' => 'HTML5', 'desc' => 'HTML5 video / audio adapter', 'file' => 'snippet.html5.php', 'props' => 'properties.html5.php'), + array('name' => 'Twitch', 'desc' => 'Twitch channel & video adapter', 'file' => 'snippet.twitch.php', 'props' => 'properties.twitch.php'), +); -$snippet = $modx->newObject('modSnippet'); -$snippet->fromArray(array( - 'name' => 'Vimeo', - 'description' => 'Vimeo adapter', - 'snippet' => getSnippetContent($sources['elements'].'snippets/snippet.vimeo.php'), -), '', true, true); -$snippets[] = $snippet; +foreach ($defs as $def) { + $snippet = $modx->newObject('modSnippet'); + $snippet->fromArray(array( + 'name' => $def['name'], + 'description' => $def['desc'], + 'snippet' => getSnippetContent($sources['elements'] . 'snippets/' . $def['file']), + ), '', true, true); -$snippet = $modx->newObject('modSnippet'); -$snippet->fromArray(array( - 'name' => 'SoundCloud', - 'description' => 'SoundCloud adapter', - 'snippet' => getSnippetContent($sources['elements'].'snippets/snippet.soundcloud.php'), -), '', true, true); -$properties = include $sources['data'].'properties/properties.soundcloud.php'; -$snippet->setProperties($properties); -$snippets[] = $snippet; + if ($def['props']) { + $properties = include $sources['data'] . 'properties/' . $def['props']; + $snippet->setProperties($properties); + } -$snippet = $modx->newObject('modSnippet'); -$snippet->fromArray(array( - 'name' => 'HTML5', - 'description' => 'HTML5 video adapter', - 'snippet' => getSnippetContent($sources['elements'].'snippets/snippet.html5.php'), -), '', true, true); -$properties = include $sources['data'].'properties/properties.html5.php'; -$snippet->setProperties($properties); -$snippets[] = $snippet; - -$snippet = $modx->newObject('modSnippet'); -$snippet->fromArray(array( - 'name' => 'Twitch', - 'description' => 'Twitch video & channel adapter', - 'snippet' => getSnippetContent($sources['elements'].'snippets/snippet.twitch.php'), -), '', true, true); -$properties = include $sources['data'].'properties/properties.twitch.php'; -$snippet->setProperties($properties); -$snippets[] = $snippet; + $snippets[] = $snippet; +} unset($snippet); -return $snippets; \ No newline at end of file +return $snippets; diff --git a/core/components/videobox/elements/snippets/snippet.srf.php b/core/components/videobox/elements/snippets/snippet.srf.php new file mode 100644 index 0000000..ef0e92f --- /dev/null +++ b/core/components/videobox/elements/snippets/snippet.srf.php @@ -0,0 +1,30 @@ +getOption('videobox.core_path', null, $modx->getOption('core_path') . 'components/videobox/'); +require_once $vbCore . 'model/adapters/srf.class.php'; + +$id = isset($scriptProperties['id']) ? trim($scriptProperties['id']) : ''; +if ($id === '') return false; + +if (SrfVideo::matches($id)) { + return new SrfVideo($scriptProperties); +} + +return false; diff --git a/core/components/videobox/model/adapters/srf.class.php b/core/components/videobox/model/adapters/srf.class.php new file mode 100644 index 0000000..95508d7 --- /dev/null +++ b/core/components/videobox/model/adapters/srf.class.php @@ -0,0 +1,172 @@ +urn = $resolved['urn']; + $this->embedHost = $resolved['host']; + + parent::__construct($properties); + } + + // ── Embed URL ───────────────────────────────────────────────────────────── + + public function getPlayerLink($autoplay = false) { + $src = $this->embedHost . '/play/embed?urn=' . rawurlencode($this->urn); + if ($autoplay) $src .= '&autoplay=true'; + return $src; + } + + // ── Title ───────────────────────────────────────────────────────────────── + + public function getTitle($forced = false) { + if ($forced && $this->title === '') { + return $this->urn; + } + return $this->title; + } + + // ── Thumbnail ───────────────────────────────────────────────────────────── + + public function getThumb() { + // Check for a locally overridden thumbnail first + $local = parent::getThumb(); + if ($local !== false) return $local; + + // Fetch thumbnail URL from the SRG SSR Integration Layer + $apiUrl = 'https://il.srgssr.ch/integrationlayer/2.0/mediaComposition/byUrn/' + . rawurlencode($this->urn) . '.json'; + + $ctx = stream_context_create(['http' => ['timeout' => 5, 'method' => 'GET', + 'header' => "User-Agent: VideoboxMODX/7.0\r\n"]]); + $raw = @file_get_contents($apiUrl, false, $ctx); + if (!$raw) return false; + + $data = json_decode($raw, true); + if (!$data) return false; + + // Dig into chapterList → imageUrlWithText (or similar) + $img = null; + $chapter = $data['chapterList'][0] ?? null; + if ($chapter) { + // Prefer the largest image available + $img = $chapter['imageUrlWithText'] + ?? $chapter['imageUrl'] + ?? null; + } + if (!$img) return false; + + // Append a width hint for a usable resolution + $img = rtrim($img, '/') . '/scale/640x360'; + + $im = @getimagesize($img); + if ($im !== false) return array($img, $im[2]); + + // Return the URL without size info if getimagesize failed (remote image) + return array($img, IMAGETYPE_JPEG); + } + + // ── Static helpers ──────────────────────────────────────────────────────── + + /** + * Normalise any supported SRF/SRG input to a canonical URN + embed host. + * + * @return array{urn:string, host:string} + */ + public static function resolveUrn(string $raw): array { + $raw = trim($raw); + + // Already a full URN: urn:srf:video:UUID + if (preg_match('#^urn:([a-z]+):(video|audio):([a-f0-9-]+)$#i', $raw, $m)) { + $host = self::hostForChannel($m[1]); + return ['urn' => strtolower($raw), 'host' => $host]; + } + + // Plain UUID only + if (preg_match('#^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$#i', $raw)) { + return ['urn' => 'urn:srf:video:' . strtolower($raw), 'host' => 'https://www.srf.ch']; + } + + // Full URL + $host = parse_url($raw, PHP_URL_HOST) ?? ''; + $channel = self::channelForHost($host); + $embedHost = 'https://' . $host; + + // ?urn= query parameter + parse_str(parse_url($raw, PHP_URL_QUERY) ?? '', $qs); + if (!empty($qs['urn']) && preg_match('#^urn:[a-z]+:(video|audio):#i', $qs['urn'])) { + return ['urn' => strtolower($qs['urn']), 'host' => $embedHost]; + } + + // /redirect/detail/UUID or any path ending in a UUID + $path = parse_url($raw, PHP_URL_PATH) ?? ''; + $segments = array_filter(explode('/', $path)); + $last = end($segments); + if ($last && preg_match('#^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$#i', $last)) { + return ['urn' => 'urn:' . $channel . ':video:' . strtolower($last), 'host' => $embedHost]; + } + + // Fallback: return raw as-is (caller will reject) + return ['urn' => $raw, 'host' => $embedHost]; + } + + private static function channelForHost(string $host): string { + if (str_contains($host, 'rts.ch')) return 'rts'; + if (str_contains($host, 'rtr.ch')) return 'rtr'; + if (str_contains($host, 'rsi.ch')) return 'rsi'; + if (str_contains($host, 'swissinfo.ch')) return 'swi'; + return 'srf'; + } + + private static function hostForChannel(string $channel): string { + $map = [ + 'rts' => 'https://www.rts.ch', + 'rtr' => 'https://www.rtr.ch', + 'rsi' => 'https://www.rsi.ch', + 'swi' => 'https://www.swissinfo.ch', + ]; + return $map[$channel] ?? 'https://www.srf.ch'; + } + + /** + * Detect whether a raw string looks like an SRF/SRG input. + */ + public static function matches(string $raw): bool { + // Already a URN + if (preg_match('#^urn:(srf|rts|rtr|rsi|swi):(video|audio):#i', $raw)) return true; + // Plain UUID + if (preg_match('#^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$#i', $raw)) return true; + // URL from a known SRG domain + if (preg_match('#(?:srf|rts|rtr|rsi|swissinfo)\.ch/play/#i', $raw)) return true; + return false; + } +} diff --git a/core/components/videobox/model/videobox/videobox.class.php b/core/components/videobox/model/videobox/videobox.class.php index acfc424..9fa0a64 100644 --- a/core/components/videobox/model/videobox/videobox.class.php +++ b/core/components/videobox/model/videobox/videobox.class.php @@ -84,12 +84,19 @@ function getVideo(array $props = array()){ } function loadAssets(){ - $this->modx->regClientCSS($_GET['dev'] ? '/Videobox/videobox.min.css' : $this->config['assets_url'] . 'css/videobox.min.css'); - $this->modx->regClientScript($_GET['dev'] ? '/Videobox/videobox.min.js' : $this->config['assets_url'] . 'js/videobox.bundle.js'); - - $overrides = file_get_contents($_GET['dev'] ? '/srv/htdocs/Videobox/dist/overrides.css' : $this->config['assets_path'] . 'css/overrides.min.css'); - $styleOverride = str_replace(array('#005723', '#84d1a4', '.vb-overrides-wrap'), array('#' . $this->config['tColor'], '#' . $this->config['hColor'], ''), $overrides); - $this->modx->regClientStartupHTMLBlock(''); + $this->modx->regClientCSS($this->config['assets_url'] . 'css/videobox.min.css'); + $this->modx->regClientScript($this->config['assets_url'] . 'js/videobox.bundle.js'); + + $overridesFile = $this->config['assets_path'] . 'css/overrides.min.css'; + if (is_file($overridesFile)) { + $overrides = file_get_contents($overridesFile); + $styleOverride = str_replace( + array('#005723', '#84d1a4', '.vb-overrides-wrap'), + array('#' . $this->config['tColor'], '#' . $this->config['hColor'], ''), + $overrides + ); + $this->modx->regClientStartupHTMLBlock(''); + } } function setCache($key, $data){