diff --git a/objects/Object.php b/objects/Object.php index 84a0dfee2f..6fcf27710f 100644 --- a/objects/Object.php +++ b/objects/Object.php @@ -16,11 +16,11 @@ abstract class ObjectYPT implements ObjectInterface protected $id; protected $created; - public function __construct($id = "") + public function __construct($id = "", $refreshCache = false) { if (!empty($id)) { // get data from id - $this->load($id); + $this->load($id, $refreshCache); } } @@ -29,9 +29,9 @@ abstract class ObjectYPT implements ObjectInterface return []; } - public function load($id) + public function load($id, $refreshCache = false) { - $row = self::getFromDb($id); + $row = self::getFromDb($id, $refreshCache); if (empty($row)) { return false; } diff --git a/objects/category.php b/objects/category.php index 9f439903de..86aa041773 100644 --- a/objects/category.php +++ b/objects/category.php @@ -118,7 +118,7 @@ class Category } } - public function load($id) + public function load($id, $refreshCache = false) { $row = self::getCategory($id); if (empty($row)) { diff --git a/objects/comment.php b/objects/comment.php index f48332bd28..fbd0ce71eb 100644 --- a/objects/comment.php +++ b/objects/comment.php @@ -67,7 +67,7 @@ class Comment { return $this->videos_id; } - public function load($id) { + public function load($id, $refreshCache = false) { $row = self::getComment($id); if (empty($row)) { return false; diff --git a/objects/configuration.php b/objects/configuration.php index 86e1a2f91b..2417aed7de 100644 --- a/objects/configuration.php +++ b/objects/configuration.php @@ -59,10 +59,10 @@ class AVideoConf extends ObjectYPT{ } } - public function load($id='') + public function load($id='', $refreshCache = false) { global $global; - return parent::load(1); + return parent::load(1, $refreshCache); } public function save(){ diff --git a/objects/subscribe.php b/objects/subscribe.php index c3417e738d..edbf41002d 100644 --- a/objects/subscribe.php +++ b/objects/subscribe.php @@ -39,7 +39,7 @@ class Subscribe extends ObjectYPT{ } } - public function load($id) + public function load($id, $refreshCache = false) { $obj = self::getSubscribe($id); if (empty($obj)) { diff --git a/objects/user.php b/objects/user.php index bd24f6a7d2..7997032d31 100644 --- a/objects/user.php +++ b/objects/user.php @@ -277,7 +277,7 @@ if (typeof gtag !== \"function\") { return $eo[$id]; } - public function load($id) + public function load($id, $refreshCache = false) { $id = intval($id); if (empty($id)) { diff --git a/objects/userGroups.php b/objects/userGroups.php index ba7072363a..6167ae8dc8 100644 --- a/objects/userGroups.php +++ b/objects/userGroups.php @@ -23,7 +23,7 @@ class UserGroups{ } } - public function load($id) + public function load($id, $refreshCache = false) { $user = self::getUserGroupsDb($id); if (empty($user)) { diff --git a/plugin/SocialMediaPublisher/Objects/Publisher_video_publisher_logs.php b/plugin/SocialMediaPublisher/Objects/Publisher_video_publisher_logs.php index 7f184ff8fe..5541037262 100644 --- a/plugin/SocialMediaPublisher/Objects/Publisher_video_publisher_logs.php +++ b/plugin/SocialMediaPublisher/Objects/Publisher_video_publisher_logs.php @@ -8,6 +8,12 @@ class Publisher_video_publisher_logs extends ObjectYPT protected $id, $publish_datetimestamp, $status, $details, $videos_id, $users_id, $publisher_social_medias_id, $timezone; + const STATUS_UNVERIFIED = 'u'; + const STATUS_VERIFIED = 'v'; + const STATUS_ACTIVE = 'a'; + const STATUS_INACTIVE = 'i'; + const STATUS_PROCESSING = 'p'; + static function getSearchFieldsNames() { return array('details', 'timezone'); @@ -199,6 +205,7 @@ class Publisher_video_publisher_logs extends ObjectYPT static function getInfo($row) { + global $global; $row['publish'] = date('Y-m-d H:i:s', $row['publish_datetimestamp']); $row['json'] = json_decode($row['details']); @@ -229,6 +236,18 @@ class Publisher_video_publisher_logs extends ObjectYPT $link = "https://www.facebook.com/watch/?v=" . $row['json']->response->VideoUploadResponse->id; $msg[] = "{$link}"; } + break; + case SocialMediaPublisher::SOCIAL_TYPE_INSTAGRAM["name"]: + if(!empty($row['json']->mediaResponse->permalink)){ + $msg[] = "mediaResponse->permalink}' target='_blank'>{$row['json']->mediaResponse->permalink}"; + }else if ($row['status'] === self::STATUS_UNVERIFIED) { + $msg[] = ' Video is being processed: Your video is currently being processed for publishing on Instagram. Please wait.'; + } elseif ($row['status'] === self::STATUS_VERIFIED) { + $msg[] = ' Video successfully published: Your video has been verified and uploaded to Instagram.'; + } else { + $msg[] = ' status: ' . $row['status'] . ''; + } + break; } $row['msg'] = implode('
', $msg); @@ -251,4 +270,12 @@ class Publisher_video_publisher_logs extends ObjectYPT sqlDAL::close($res); return $countRow; } + + public function save() + { + if (empty($this->status)) { + $this->status = self::STATUS_UNVERIFIED; + } + return parent::save(); + } } diff --git a/plugin/SocialMediaPublisher/Objects/SocialUploader.php b/plugin/SocialMediaPublisher/Objects/SocialUploader.php index 35c482269e..9ae9a126c9 100644 --- a/plugin/SocialMediaPublisher/Objects/SocialUploader.php +++ b/plugin/SocialMediaPublisher/Objects/SocialUploader.php @@ -25,13 +25,16 @@ class SocialUploader break; case SocialMediaPublisher::SOCIAL_TYPE_INSTAGRAM['name']: $pub = Publisher_user_preferences::getFromDb($publisher_user_preferences_id); + $broadcaster_id = 0; if (!empty($pub)) { $json = json_decode($pub['json']); + //var_dump($json); if (!empty($json) && !empty($json->{"restream.ypt.me"}->instagram) && !empty($json->{"restream.ypt.me"}->instagram->access_token)) { $accessToken = $json->{"restream.ypt.me"}->instagram->access_token; + $broadcaster_id = $json->{"restream.ypt.me"}->instagram->broadcaster_id; } } - return SocialUploader::uploadInstagram($accessToken, $videoPath, $title, $description, $isShort); + return SocialUploader::uploadInstagram($accessToken, $videoPath, $title, $description, $broadcaster_id); break; case SocialMediaPublisher::SOCIAL_TYPE_TWITCH['name']: //return SocialUploader::uploadYouTube($accessToken, $videoPath, $title, $description, $visibility, $isShort); @@ -151,10 +154,10 @@ class SocialUploader } } - private static function uploadInstagram($accessToken, $videoPath, $title, $description, $isShort = false) + private static function uploadInstagram($accessToken, $videoPath, $title, $description, $broadcaster_id) { $caption = $title . PHP_EOL . PHP_EOL . $description; - return InstagramUploader::upload($accessToken, $videoPath, $caption, $isShort); + return InstagramUploader::upload($accessToken, $videoPath, $caption, $broadcaster_id); } static public function getErrorMsg($obj) @@ -846,81 +849,150 @@ class LinkedInUploader class InstagramUploader { /** - * Upload a video to Instagram. + * Upload and publish a video to Instagram. * - * @param string $accessToken Instagram access token. - * @param string $videoPath video file. + * @param string $accessToken Instagram user access token. + * @param string $videoUrl Public URL to the video file. * @param string $caption Caption for the video. - * @param string $userId Instagram user ID. + * @param string $instagramAccountId Instagram Business Account ID. * @return array Response from the Instagram API. */ - public static function upload($accessToken, $videoPath, $caption, $userId) + public static function upload($accessToken, $videoUrl, $caption, $instagramAccountId) { - global $global; $return = [ 'error' => true, 'msg' => '', - 'initResponse' => null, + 'containerId' => null, 'publishResponse' => null ]; - $videoUrl = str_replace($global['systemRootPath'], $global['webSiteRootURL'], $videoPath); - - $videoUrl = addQueryStringParameter($videoUrl, 'globalToken', getToken(30)); - - // Step 1: Initialize the upload with video_url - $initResponse = self::initializeInstagramUpload($accessToken, $userId, $videoUrl, $caption); - $return['initResponse'] = $initResponse; - - if ($initResponse['error']) { - $return['msg'] = "Failed to initialize Instagram upload: " . $initResponse['msg']; + // Step 1: Create Media Container + $containerResponse = self::createMediaContainer($accessToken, $videoUrl, $caption, $instagramAccountId); + if ($containerResponse['error']) { + $return['msg'] = 'Error creating media container: ' . $containerResponse['msg']; return $return; } - $containerId = $initResponse['containerId']; + $containerId = $containerResponse['id']; + $return['accessToken'] = $accessToken; + $return['containerId'] = $containerId; + $return['containerResponse'] = $containerResponse; + $return['instagramAccountId'] = $instagramAccountId; + + if (!empty($return['containerId'])) { + $return['error'] = false; + } + + $waitForMediaProcessing = self::waitForMediaProcessing($accessToken, $containerId); + $return['waitForMediaProcessing'] = $waitForMediaProcessing; + return $return; + } + + private static function createMediaContainer($accessToken, $videoUrl, $caption, $instagramAccountId) + { + global $global; + $url = "https://graph.facebook.com/{$instagramAccountId}/media"; + + $videoUrl = str_replace($global['systemRootPath'], $global['webSiteRootURL'], $videoUrl); + $data = [ + 'media_type' => 'REELS', + 'video_url' => $videoUrl, + 'is_carousel_item' => false, + 'caption' => $caption, + 'access_token' => $accessToken + ]; + + $response = self::makeCurlRequest($url, $data); + + if ($response['httpCode'] !== 200 || empty($response['response']['id'])) { + return [ + 'error' => true, + 'msg' => $response['response']['error']['message'] ?? 'Failed to create media container.', + 'url' => $url, + 'data' => $data, + 'response' => $response + ]; + } + + return ['error' => false, 'id' => $response['response']['id'], 'url' => $url, 'data' => $data, 'response' => $response]; + } + + + public static function publishMediaIfIsReady($accessToken, $containerId, $instagramAccountId) + { + + $return = [ + 'error' => true, + 'msg' => '', + 'containerId' => null, + 'publishResponse' => null + ]; + $return['accessToken'] = $accessToken; + $return['containerId'] = $containerId; + $return['instagramAccountId'] = $instagramAccountId; + + $waitForMediaProcessing = self::waitForMediaProcessing($accessToken, $containerId); + $return['waitForMediaProcessing'] = $waitForMediaProcessing; + //var_dump($isReady);exit; + + if(empty($waitForMediaProcessing['error']) || $waitForMediaProcessing["response"]["status_code"] === "PUBLISHED"){ + // Step 3: Publish Media + $publishResponse = self::publishMedia($accessToken, $containerId, $instagramAccountId); + + $mediaResponse = self::getInstagramVideoLink($publishResponse['id'], $accessToken); + + $return['publishResponse'] = $publishResponse; + $return['mediaResponse'] = $mediaResponse; + } + + if ($waitForMediaProcessing['error']) { + $return['msg'] = $waitForMediaProcessing['msg']; + return $return; + } - // Step 2: Publish the video - $publishResponse = self::publishInstagramVideo($accessToken, $containerId, $userId); - $return['publishResponse'] = $publishResponse; if ($publishResponse['error']) { - $return['msg'] = "Error publishing video on Instagram: " . $publishResponse['msg']; + $return['msg'] = 'Error publishing media: ' . $publishResponse['msg']; return $return; } $return['error'] = false; $return['msg'] = 'Video uploaded and published successfully!'; + $return['publishResponse'] = $publishResponse; return $return; } - private static function initializeInstagramUpload($accessToken, $userId, $videoUrl, $caption) + private static function waitForMediaProcessing($accessToken, $containerId, $maxAttempts = 1) { - $url = "https://graph.facebook.com/$userId/media"; - - $data = [ - 'media_type' => 'VIDEO', - 'video_url' => $videoUrl, - 'caption' => $caption, - 'access_token' => $accessToken, + $url = "https://graph.facebook.com/{$containerId}?fields=status_code,status,id&access_token={$accessToken}"; + $return = [ + 'error' => true, + 'msg' => '', + 'response' => null, + 'url' => $url, ]; - $response = self::makeCurlRequest($url, $data); - //var_dump($url, $data, $response);exit; + $attempts = 0; + do { + sleep(5); // Wait for 5 seconds + $response = self::makeCurlRequest($url); + $status = $response['response']['status_code'] ?? null; + $return['response'] = $response['response']; + if ($status === 'FINISHED') { + $return['error'] = false; + return $return; + } + if (!empty($return['response']['error']) && !empty($return['response']['error']['message'])) { + $return['msg'] = $return['response']['error']['message']; + } - if ($response['httpCode'] !== 200 || empty($response['response']['id'])) { - return [ - 'error' => true, - 'msg' => $response['response']['error']['message'] ?? 'Failed to initialize upload.' - ]; - } - - return [ - 'error' => false, - 'containerId' => $response['response']['id'] - ]; + $attempts++; + } while ($attempts < $maxAttempts); + _error_log("waitForMediaProcessing($accessToken, $containerId, $maxAttempts) " . json_encode($response)); + return $return;; } - private static function publishInstagramVideo($accessToken, $containerId, $userId) + private static function publishMedia($accessToken, $containerId, $instagramAccountId) { - $url = "https://graph.facebook.com/v17.0/$userId/media_publish"; + $url = "https://graph.facebook.com/v17.0/{$instagramAccountId}/media_publish"; $data = [ 'creation_id' => $containerId, @@ -932,31 +1004,67 @@ class InstagramUploader if ($response['httpCode'] !== 200) { return [ 'error' => true, - 'msg' => $response['response']['error']['message'] ?? 'Failed to publish video.' + 'msg' => $response['response']['error']['message'] ?? 'Failed to publish media.' + ]; + } + + return ['error' => false, 'id' => $response['response']['id']]; + } + + private static function makeCurlRequest($url, $data = []) + { + $ch = curl_init($url); + + if (!empty($data)) { + // POST request if $data is not empty + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } else { + // GET request if $data is empty + curl_setopt($ch, CURLOPT_HTTPGET, true); + } + + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Optional: Timeout after 30 seconds + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); + curl_close($ch); + + if ($response === false) { + return [ + 'httpCode' => $httpCode, + 'response' => ['error' => ['message' => $error]] ]; } return [ - 'error' => false, - 'msg' => 'Video published successfully.', - 'response' => $response['response'] + 'httpCode' => $httpCode, + 'response' => json_decode($response, true) ]; } - private static function makeCurlRequest($url, $data) + static function getInstagramVideoLink($mediaId, $accessToken) { - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $data); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + $url = "https://graph.facebook.com/{$mediaId}?fields=permalink&access_token={$accessToken}"; + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = curl_error($ch); curl_close($ch); - return [ - 'response' => json_decode($response, true), - 'httpCode' => $httpCode - ]; + if ($response === false) { + return ['error' => true, 'msg' => 'Error fetching permalink: ' . $error]; + } + + $data = json_decode($response, true); + + if (isset($data['permalink'])) { + return ['error' => false, 'permalink' => $data['permalink']]; + } else { + return ['error' => true, 'msg' => 'Failed to retrieve video link.']; + } } } diff --git a/plugin/SocialMediaPublisher/SocialMediaPublisher.php b/plugin/SocialMediaPublisher/SocialMediaPublisher.php index 6272e32410..e6abc42317 100644 --- a/plugin/SocialMediaPublisher/SocialMediaPublisher.php +++ b/plugin/SocialMediaPublisher/SocialMediaPublisher.php @@ -50,9 +50,9 @@ class SocialMediaPublisher extends PluginAbstract self::SOCIAL_TYPE_LINKEDIN['name'] => self::SOCIAL_TYPE_LINKEDIN, ); - //const RESTREAMER_URL = 'https://restream.ypt.me/'; + const RESTREAMER_URL = 'https://restream.ypt.me/'; //const RESTREAMER_URL = 'http://localhost:81/Restreamer/'; - const RESTREAMER_URL = 'https://vlu.me:444/Restreamer/'; + //const RESTREAMER_URL = 'https://vlu.me:444/Restreamer/'; public function getTags() { @@ -296,7 +296,7 @@ class SocialMediaPublisher extends PluginAbstract return $response; } - private static function saveLog($publisher_social_medias_id, $videos_id, $details, $users_id = 0, $status = '') + private static function saveLog($publisher_social_medias_id, $videos_id, $details, $users_id = 0, $status = Publisher_video_publisher_logs::STATUS_UNVERIFIED) { if (empty($users_id)) { $users_id = User::getId(); @@ -346,4 +346,55 @@ class SocialMediaPublisher extends PluginAbstract return $btn; } + + static function scanInstagam() + { + global $global; + $sql = "SELECT psm.*, pvpl.* FROM publisher_video_publisher_logs pvpl LEFT JOIN publisher_social_medias psm ON publisher_social_medias_id = psm.id + WHERE pvpl.status='" . Publisher_video_publisher_logs::STATUS_UNVERIFIED . "' + AND name = '" . SocialMediaPublisher::SOCIAL_TYPE_INSTAGRAM["name"] . "' ORDER BY pvpl.id DESC LIMIT 100 "; + + $res = sqlDAL::readSql($sql); + $fullData = sqlDAL::fetchAllAssoc($res); + sqlDAL::close($res); + //var_dump($sql, $fullData); + if (!empty($fullData)) { + foreach ($fullData as $key => $row) { + $json = json_decode($row["details"]); + + $accessToken = $json->response->accessToken; + $containerId = $json->response->containerId; + $instagramAccountId = $json->response->instagramAccountId; + + $obj = InstagramUploader::publishMediaIfIsReady($accessToken, $containerId, $instagramAccountId); + + if ((isset($obj['error']) && $obj['error'] === false) || $obj["waitForMediaProcessing"]["response"]["status_code"] === "PUBLISHED") { + + //var_dump($obj); + if (!empty($obj['publishResponse'])) { + $json->publishResponse = $obj['publishResponse']; + } + if (!empty($obj['mediaResponse'])) { + $json->mediaResponse = $obj['mediaResponse']; + } + + $smp = new Publisher_video_publisher_logs($row['id'], true); + $smp->setDetails($json); + $smp->setStatus(Publisher_video_publisher_logs::STATUS_VERIFIED); + if ($smp->save()) { + $poster = Video::getPoster($row["videos_id"],); + $img = ""; + sendSocketMessageToUsers_id('Video published on instagram
' . $img, $row["users_id"], 'avideoToastSuccess'); + } + } + return $obj; + } + } + return false; + } + + function executeEveryMinute() + { + self::scanInstagam(); + } } diff --git a/plugin/SocialMediaPublisher/publishInstagram.json.php b/plugin/SocialMediaPublisher/publishInstagram.json.php new file mode 100644 index 0000000000..c3f68ebcd2 --- /dev/null +++ b/plugin/SocialMediaPublisher/publishInstagram.json.php @@ -0,0 +1,14 @@ +error = true; +$obj->msg = ''; + +$plugin = AVideoPlugin::loadPluginIfEnabled('SocialMediaPublisher'); + +$obj = SocialMediaPublisher::scanInstagam(); + +die(json_encode($obj)); diff --git a/view/js/script.js b/view/js/script.js index a23d165897..7ddc798fbc 100644 --- a/view/js/script.js +++ b/view/js/script.js @@ -1388,7 +1388,7 @@ function avideoAlertAJAXHTML(url) { modal.showPleaseWait(); $.ajax({ url: url, - success: function (response) { + complete: function (response) { avideoAlertText(response); modal.hidePleaseWait(); } @@ -1399,7 +1399,7 @@ function avideoAlertAJAX(url) { modal.showPleaseWait(); $.ajax({ url: url, - success: function (response) { + complete: function (response) { avideoResponse(response); modal.hidePleaseWait(); }