Как стать автором
Обновить

«Афиша7» – интегрировать 4 API, чтобы разродиться собственным

PHP *Yii *API *ВКонтакте API *Яндекс API *
Из песочницы

«Не пора ли, друзья мои, нам замахнуться на Вильяма, понимаете ли, м-м, нашего Шекспира?»

«Афиша7» – проект о культурно-развлекательных событиях России.

В конце декабря 2019 года передо мной стояла задача разработки агрегатора культурных событий. Решение предполагало создание 2х инструментов: для добавления мероприятий на сайте, для сбора и предъявления актуальных данных о планируемых событиях.

В обзоре я кратко опишу основные этапы разработки, сложности реализации проекта.

Идея проекта

Новостные сайты, социальные сети заполнены постами о прошедших мероприятиях в городе, регионе. Довольно часто посещала мысль – жаль, что не узнал заранее, было бы неплохо посетить. Информация о предстоящих событиях практически недоступна, появляется слишком поздно, чтобы принять решение о его посещении, или приходится отслеживать много различных сайтов. Особенно сложно найти информацию куда сходить в другом городе, находясь в отпуске, командировке.

Так возникла амбициозная идея собрать данные со всех возможных источников культурно-развлекательных событий в одном месте и предоставить возможность организаторам добавлять информацию непосредственно на сайте.

Цель проекта

Цель проекта сформулирована следующим образом: представить максимальное количество актуальных событий по каждому населенному пункту РФ в едином интерфейсе.

API PRO.Культура.рф

На начальном этапе основным источником актуальной информации по всем регионам России был выбран проект PRO.Культура.рф (с декабря 2019 года почти 700 тыс. уникальных событий).

Интеграция в первую очередь именно API PRO.Культура.рф позволило определиться со структурой сайта, отработать полную схему взаимодействия – как получение данных, так и отправку. В результате, справочники регионов, населенных пунктов, категорий событий и мест проведения, справочник мест проведения PRO.Культура.рф были взяты за основу справочников сайта.

Прототип интеграции функционала API PRO.Культура.рф

Функция получения справочника категорий (событий и мест)

function getCategories()
{
	$countIns=0;
	$url='https://pro.culture.ru/api/2.4/categories?apiKey='.OurAPIKEY.'&limit=100';
	foreach(['places', 'events'] as $type)
	{
		$response=file_get_contents($url."&type=$type");
		if ($response) $arrType[$type]=json_decode($response); else $arrType[$type]=NULL;
	}
	foreach($arrType as $type=>$arr)
	{
		if($arr->categories)
		{
			$arrInsert=NULL;
			foreach($arr->categories as $obj)
			{
				if ($obj->_id>0 && in_array($obj->type, ['places', 'events'])) $arrInsert[]=['cat_id'=>$obj->_id, 'name'=>$obj->name, 'url'=>$obj->sysName, 'num'=>$obj->index, 'type'=>$obj->type ];
			}
			if ($arrInsert)
			{
				$countIns+=Yii::$app->db->createCommand()->batchInsert('nsi_categories', ['cat_id', 'name',  'url', 'num', 'type'], $arrInsert)->execute();
			}
		}
	}
	return $countIns;
}

Функция получения справочника локалей (населенных пунктов)

function getlocales()
{
	$maxRows=1000; $countIns=0;
	$url='https://pro.culture.ru/api/2.4/locales?apiKey='.OurAPIKEY;
	$response=file_get_contents($url.'&limit=1');
	if ($response) $arrResponse=json_decode($response);
	if ($arrResponse) $count=$arrResponse->total; else return 0;
	unset($arrResponse);
	for($i=0; $i<ceil($count/$maxRows); $i++)
	{
		$arrInsert=NULL;
		$link=$url.'&offset='.($i*$maxRows)."&limit=$maxRows";
		$response=json_decode(file_get_contents($link));
		foreach($response->locales as $obj)
		{
			$arrInsert[]=['ids'=>$obj->_id, 'parent_id'=>$obj->parentId, 'name'=>$obj->name, 'url'=>$obj->sysName, 'population'=>$obj->population, 'fias_id'=>$obj->fias->_id, 'fias_name'=>$obj->fias->name, 'fias_level'=>$obj->fias->level ];
		}
		if ($arrInsert)
		{
			$countIns+=Yii::$app->db->createCommand()->batchInsert('nsi_locales', ['ids', 'parent_id', 'name', 'url', 'population', 'fias_id', 'fias_name', 'fias_level'], $arrInsert)->execute();
		}
	}
	return $countIns;
}

Функция получения справочника мест проведения мероприятий

function getPlaces()
{
	$arr=NULL;
	$maxRows=100;
	$model=new Places();
	$countIns=0;
	$url='https://pro.culture.ru/api/2.4/places?apiKey='.OurAPIKEY.'&sort=_id&status=accepted';
	if (Yii::$app->request->get('date')) $date=Yii::$app->request->get('date')*1000;
	if ($date) $url.='&startUpdateDate='.$date;
	$response=file_get_contents($url'.&limit=1');
	if ($response) $arrResponse=json_decode($response);
	if ($arrResponse) $count=$arrResponse->total; else return 0;
	unset($arrResponse);
	$arrCats=(new Categories())->getcategories();//список выбранных категорий мест
	for($i=0; $i<ceil($count/$maxRows); $i++)//формирование $arr[] - массив массивов ответов сервера с $maxRows=100
	{
		$link=$url.'&offset='.($i*$maxRows)."&limit=$maxRows";
		$response=json_decode(file_get_contents($link));
		$arrInsert=NULL;
		$arrCon=NULL;
		foreach($response->places as $obj)
		{//перебор каждой записи как объекта
			$arr=NULL;
			$strGallery='';//строка для формирования поля gallery
			$strAddress='';//строка для формирования поля address
			if ($obj->gallery)
			{
				$gal=$obj->gallery;
				foreach($gal as $row) $arrgg[]=$row->name;
				$strGallery=implode(',', $arrgg);
				$arrgg=NULL;
			}
			if ($obj->contacts)
			{//формирование $arrCon для добавления в таблицу nsi_places_contacts
				foreach ($obj->contacts as $key=>$val)
				{
					if ($key=='phones') foreach($val as $phones) $arrCon[]=[$obj->_id, $phones->value, $phones->comment, $key]; else $arrCon[]=[$obj->_id, $val, '', $key];
				}
			}
			if ($obj->address)
			{//фомирование строки адреса для поля address
				if ($obj->address->region) $strAddress.=$obj->address->region->name .' '. $obj->address->region->type .', ';
				if ($obj->address->area) $strAddress.=$obj->address->area->name .' '. $obj->address->area->type .', ';
				if ($obj->address->city) $strAddress.=$obj->address->city->type .'. '. $obj->address->city->name .', ';
				if ($obj->address->settlement) $strAddress.=$obj->address->settlement->type .'. '. $obj->address->settlement->name .', ';
				if ($obj->address->street) $strAddress.=$obj->address->street->type .'. '. $obj->address->street->name .', ';
				if ($obj->address->house) $strAddress.=$obj->address->house->name;
				$strAddress=trim($strAddress);
				$strAddress=rtrim($strAddress, '!,.-');
			}
			if ($obj->workingSchedule)
			{//анализ расписания и формирование в виде "ЧЧ:ММ - ЧЧ:ММ" для каждого дня недели.
				for ($d=0; $d<7; $d++ )
				{
					$strSched='shed_'.$d;
					if ($obj->workingSchedule->$d)
					{
						$from=$obj->workingSchedule->$d->from;
						$to=$obj->workingSchedule->$d->to;
						if ($from*$to>0)
						{
							$from=$from/60000;
							$fh=intval($from/60);
							$fm=$from-$fh*60;
							$to=$to/60000;
							$th=intval($to/60);
							$tm=$to-$th*60;
							$$strSched=sprintf("%02d:%02d - %02d:%02d", $fh, $fm, $th, $tm);							
						} else $$strSched='';
					}
				}
			}
			$arrInsert[]=['ids'=>$obj->_id, 'description'=>$obj->description, 'name'=>$obj->name, 'altname'=>$obj->altName, 'img_name'=>$obj->image->name, 'img_realname'=>$obj->image->realName, 'cat_id'=>$arrCats[$obj->category->sysName], 'gallery'=>$strGallery, 'locale_id'=>$obj->locale->_id, 'mapposition'=>implode('|', $obj->mapPosition->coordinates), 'status'=>$obj->status, 'updatedate'=>$obj->updateDate/1000, 'address'=>$strAddress, 'shed_0'=>$shed_0,  'shed_1'=>$shed_1, 'shed_2'=>$shed_2, 'shed_3'=>$shed_3, 'shed_4'=>$shed_4, 'shed_5'=>$shed_5, 'shed_6'=>$shed_6];//сформировал новую запись
		}
		if ($arrInsert)
		{
			$countIns+=Yii::$app->db->createCommand()->batchInsert($model->tableName(), ['ids', 'description', 'name', 'altname', 'img_name', 'img_realname', 'cat_id', 'gallery', 'locale_id', 'mapposition', 'status', 'updatedate', 'address', 'shed_0', 'shed_1', 'shed_2', 'shed_3', 'shed_4', 'shed_5', 'shed_6'], $arrInsert)->execute();//вставка сформированных записей
		}
		if ($arrCon)
		{
			Yii::$app->db->createCommand()->batchInsert('nsi_places_contacts', ['con_ids', 'con_value', 'con_comm', 'slug' ], $arrCon)->execute();//вставка сформированных записей
		}
	}
	return $countIns;
}

Сложность возникла лишь при первоначальном заполнении событиями базы данных – чтобы сразу заполнить максимальное количество городов было принято решение загрузить мероприятия за 8 месяцев.

В августе 2020 года «Афиша7» получила статус официального партнера проекта PRO.Культура.рф (пост в официальном блоге здесь).

Наши пользователи

Реализация функционала добавления событий на сайте позволило привлечь пользователей из 64 регионов, которые опубликовали более 500 событий по 120 населенным пунктам. Инструкция публикации событий доступна по ссылке.

В этот момент пришло осознание, что полностью отсутствуют такие категории как кино, цирк, спорт, не представлены коммерческие театры, концерты.

Несомненно, функциональный портал обязан обеспечить онлайн покупку билетов.

Интеграция API «Rambler-касса» и «Яндекс.Афиша»

Чтобы заполнить наиболее популярные категории событий (кино, спорт, театры, концерты) было принято решение получать события с «Рамблер-касса», а затем и «Яндекс.Афиша».

Прототип интеграции функционала API Яндекс.Афиши

Функция получения архива с данными

function getYandexJson($ticket)
{	$res=NULL;
	$path=BASE_PATH . '/json/ya/';
	$fn='new.gz';
	$ffn=$path.$fn;
	$url='https://afisha.yandex.ru/export/json';
	$curl = curl_init();
	curl_setopt($curl, CURLOPT_RETURNTRANSFER,true);
	curl_setopt($curl, CURLOPT_HTTPHEADER, ['Authorization: Token '.OurAPIKEY]);
	curl_setopt($curl, CURLOPT_URL, $url);
	$out = curl_exec($curl);
	curl_close($curl);
	$json=json_decode($out);
	if ($json[0]->url)
	{
		$ReadFile = fopen ($json[0]->url, "rb");
		if ($ReadFile)
		{
			$WriteFile = fopen ($ffn, "wb");
			if ($WriteFile)
			{
				while(!feof($ReadFile))
				{
					fwrite($WriteFile, fread($ReadFile, 4096 ));
				}
				fclose($WriteFile);
			}
			fclose($ReadFile);
		}
		if (file_exists($ffn))
		{
			$command='tar xzvpf '.$ffn.' -C '.$path;
			$res=shell_exec($command);
		}
	}
	return $res;
}

Функция получения мест

function getPlacesYa($cityid=0)
{
	$path=BASE_PATH . '/json/ya/';
	$countInsert=0;
	$errors=NULL;
	$success=NULL;
	$filename='venues.json';
	$arrCities=Nsicities()::getCities();
	$arrPlaces=Nsiplaces::getPlaces();
	$cities=scandir($path);
	foreach($cities as $townid)
	{
		$arrInsert=NULL;
		if (in_array($townid, $arrCities) && file_exists($path.$townid.'/'.$filename))
		{
			$json=file_get_contents($path.$townid.'/'.$filename);
			$arr=json_decode($json);
			foreach($arr as $obj)
			{
			$arrPhones=NULL;
				$strPhones='';
				$arrMetro=NULL;
				$strMetro='';
				$longitude='0';
				$latitude='0';
				$strTags='prochee';
				$strLogo='';
				$strLinks='';
				$strSynonyms='';
				if (in_array($obj->id, $arrPlaces[$townid])) continue;
				if ($obj->metro) 
				{
					foreach($obj->metro as $om)
					{
						if ($om->name!='null') $arrMetro[]=$om->line_color.','.$om->name;
					}
					$strMetro=implode('|',$arrMetro);
				}
				if ($obj->phones)
				{
					foreach($obj->phones as $oph)
					{
						$arrPhones[]=$oph->text;
					}
					$strPhones=implode(',', $arrPhones);
				}
				if ($obj->links) $strLinks=implode('|', $obj->links);
				if ($obj->synonyms) $strSynonyms=implode('|', $obj->synonyms);
				if ($obj->location)
				{
					$latitude=$obj->location[1];
					$longitude=$obj->location[0];
				}
				if ($obj->tags)
				{
					$strTags=implode('|', $obj->tags);
				}
				if ($obj->image->origin_url && $obj->image->type=='image') $strLogo=$obj->image->origin_url;
				$arrInsert[]=[$obj->id, $obj->city_id, $obj->url, $obj->capacity, $obj->description, $obj->title, $strSynonyms, $longitude, $latitude, $obj->address, $strLogo, $strLinks, $strPhones, $strMetro, $strTags];
				
			}
			if ($arrInsert)
			{
				$countInsert+=Yii::$app->db->createCommand()->batchInsert('nsi_places_ya', ['id_ya', 'city_id', 'url', 'capacity', 'description', 'title', 'synonyms', 'longitude', 'latitude', 'address', 'logo', 'links', 'phones', 'metro', 'tags'], $arrInsert)->execute();
			}
		}
	}
	return $countInsert;
}
Прототип интеграции функционала API Rambler-кассы

Функция получения городов

public function getCitiesRb()
{
	$arrInsert=NULL;
	$count=0;
	$url='http://api.kassa.rambler.ru/v3/cities?WidgetKey='.MyWidgetKey;
	$curl = curl_init();
	curl_setopt($curl, CURLOPT_RETURNTRANSFER,true);
	curl_setopt($curl, CURLOPT_HTTPHEADER, ['X-ApiAuth-PartnerKey: '.MyPartnerKey]);
	curl_setopt($curl, CURLOPT_URL, $url);
	$out = curl_exec($curl);
	$cities=json_decode($out);
	curl_close($curl);
	foreach ($cities as $city)
	{
		$arrInsert[]=[$city->Id, $city->Name, $city->Coordinates->Latitude, $city->Coordinates->Longitude];
	}
	if ($arrInsert)	$count=Yii::$app->db->createCommand()->batchInsert('nsi_cities', ['id', 'name', 'latitude', 'longitude'], $arrInsert)->execute();
	return $count;		
}

Функция получения мест

function getPlacesrb()
{
	$count=0;
	$cities=Nsicities::getCities();
	$arrPlaces=Nsiplaces::getPlacesRb();
	if($cities )
	{
		foreach ($cities as $city)
		{
			$arrInsert=NULL;
			$curl = curl_init();
			$url='http://api.kassa.rambler.ru/v3/places?CityId='.$city['id'].'&WidgetKey='.MyWidgetKey;
			curl_setopt($curl, CURLOPT_RETURNTRANSFER,true);
			curl_setopt($curl, CURLOPT_HTTPHEADER, ['X-ApiAuth-PartnerKey: '.MyApiKey]);
			curl_setopt($curl, CURLOPT_URL, $url);
			$out = curl_exec($curl);
			curl_close($curl);
			if ($out)
			{
				$arr=json_decode($out);
				foreach($arr as $obj)
				{
					$lat='';
					$long='';
					$hascodescanner=0;
					$metro='';
					$phones='';
					$cinemachain='';
					if ($obj->Id)
					{
						if (in_array($obj->Id, $arrPlaces)) continue;
						if ($obj->Coordinates->Latitude) $lat=$obj->Coordinates->Latitude;
						if ($obj->Coordinates->Longitude) $long=$obj->Coordinates->Longitude;
						if ($obj->HasCodeScanner) $hascodescanner=1;
						if ($obj->MetroStations) $metro=json_encode($obj->MetroStations, JSON_UNESCAPED_UNICODE);
						if ($obj->Phones) $phones=json_encode($obj->Phones, JSON_UNESCAPED_UNICODE);
						if ($obj->CinemaChain) $cinemachain=json_encode($obj->CinemaChain, JSON_UNESCAPED_UNICODE); 
						$arrInsert[]=[$obj->Id, $obj->CityId, $obj->Type, $obj->Name, $obj->Address, $lat, $long, $metro, $phones, $obj->ImageUrl, $hascodescanner, $cinemachain];
					}
				}
				if ($arrInsert) $count+=Yii::$app->db->createCommand()->batchInsert('nsi_places_rb', ['id', 'city_id', 'type', 'name', 'address', 'latitude', 'longitude', 'metro', 'phones', 'img', 'hascodescanner', 'cinemachain'], $arrInsert)->execute();
			}
		}
	}
	return $count;
}

Основная сложность – объединить потоки данных из разных источников в единую структуру.

После интеграции «Афиша7» с «Rambler-касса» посредством API на сайте было опубликовано более 40 тысяч событий и появилась возможность покупки билетов через виджеты.

Интеграция с «Яндекс.Афиша» добавило на сайт почти 70 тысяч событий и альтернативный виджет онлайн покупки билетов.

Итак, на сайте агрегируются данные из трех источников, каждый по собственному API.

Сравнение API поставщиков событий

На сайте используется API следующих поставщиков событий:

  • PRO.Культура.РФ

  • Rambler-касса

  • Яндекс.Афиша

Все поставщики событий используют в качестве формата экспорта данных – JSON, что обусловлено компактностью и высокой степенью распространения его использования для передачи данных, требуют авторизацию для доступа к их API – использование специального партнерского ключа в каждом запросе.

На момент создания интеграции API PRO.Культура.рф инструкция представляла собой PDF файл с кратким описанием возможностей и примерами.

На текущий момент доступна полноценная документация.

Актуальная версия API – 2.5.

API Рамблер-касса сопровождается подробной документацией, позволяющая не только посмотреть подробное описание методов и параметров к ним, но и протестировать запросы к API с использованием ключей доступа, которые получают при регистрации в сервисе.

Актуальная версия API – 3.0.

Описание API Яндекс.Афиши высылают по почте после регистрации и одобрения заявки на подключение к системе в форме «Афиша: Как стать партнером».

Все API сводится к получению архиву на текущий период, где в корне архива файлы словарей с городами, тегами, а в папках, соответствующих id города: файлы с событиями, местами, сеансами, подборками в городе.

Отметим существенный недостаток API Рамблер-кассы и Яндекс.Афиши – отсутствие справочников категорий событий и мест проведения. А отсутствие возможности запросить полные списки затрудняет группировку событий, при выдаче на сайте.

В отличие от остальных поставщиков событий, которые отдают все данные на время запроса, PRO.Культура.рф позволяет запрашивать данные с фильтрацией по статусу и дате обновления событий, что существенно сокращает трафик и объем ОЗУ, требуемый для обработки полученных данных.

Оптимизация

Финишный этап заключался в синхронизации справочников из четырех источников, борьбе с дублированием информации и консолидированием данных по событиям и местам, полученных из разных источников (особо сложные случаи – это события или места проведения, которые пришли одновременно с PRO.Культура.рф, Rambler-касса, Яндекс.Афиша и добавленные на сайте вручную).

В результате, мы вынесли в отдельные справочники фильмы – более 3500 записей, и места проведения мероприятий – более 40000 записей. Внедрили механизм периодической агрегации в отдельную таблицу актуальных событий, их сеансов и мест проведения, что позволило обеспечить высокую скорость выдачи информации. Загрузили гигабайты картинок, связанных с событиями и местами проведения.

Архитектура проекта позволяет комфортно существовать на виртуальном хостинге компании «Beget», за что ей огромное спасибо.

Стек технологий

PHP7.1, MySQL 5.7, Yii2, JSCript, Bootstrap 3.

Монетизация

Для монетизации нашего проекта был разработан собственный API, проведена интеграция с ВКонтакте API, ведется разработка официального мобильного приложения.

Необходимость API «Афиша7» возникла при возникновении задачи разработать мобильное приложение.

API представляет в формате json справочники портала и актуальные события по API-key, который может запросить зарегистрированный пользователь.

Прототип API Афиша7:

Функция получения категорий событий

function getCategoriesEvents($all=FALSE)
{
	if (!$all) $where=" where  v=1 "; else $where="where 1 ";
	$where.=' and type="events" ';
	$sql='select * from nsi_categories '.$where.' order by name';
	$rows=$this->db->query($sql);
	while ($row=$rows->fetch_assoc())
	{
		$res[]=$row;
	}
	return $res;
}

Функция получения списка локалей (справочник населенных пунктов)

function getLocales()
{
	$where =' where fias_level in (1, 3) ';
	$sql='select * from nsi_locales '.$where.' order by name';
	$rows=$this->db->query($sql);
	$total=0;
	while ($row=$rows->fetch_assoc())
	{
		$res[$row['level']][]=$row;
		$total++;
	}
	if ($total>0) return ['total'=>$total, 'level'=>$res]; else return NULL;
}

Функция получения списка событий

function getEvs($loc_id=0, $cat_id=0, $limit=20, $offset=0, $from=0, $to=0)
{
	$loc_id=$loc_id*1; $cat_id=$cat_id*1; $limit=$limit*1; $offset=$offset*1; $from=$from*1; $to=$to*1; 
	if ($loc_id>0)
	{
		if ($offset) $strOffset=" OFFSET $offset "; else $strOffset='';
		if ($limit>100) return ['errors'=>'Maximum number of records 100']; elseif ($limit==0) $limit=20;
		$strLimit=" limit $limit ";
		$where=' e.locale_id='.$loc_id;
		if ($cat_id) $where.=" and e.cat_id=$cat_id";
		if($to+$from>0)
		{	if ($to>0)
			{
				if ($from>0) $where.=" and ((e.date_end>=$from and e.date_start<=$to) or e.date_end=0)";
			}
			else $where.=" and ((e.date_end>=$from) or e.date_end=0)";
		}
	} else return ['errors'=>['Not specified code of LOCALE.']];
	$sql="SELECT e.* FROM evs e WHERE $where ORDER BY e.date_start, e.date_end";
	$count=$this->db->query("select count(*) from ($sql) c ");
	$total=$count->fetch_array()[0];
	if ($total)
	{
		$rows=$this->db->query($sql.$strLimit.$strOffset);
		while ($row=$rows->fetch_assoc())
		{
			$anons=strip_tags(trim($row['anons'] ? $row['anons'] : $row['description']));
			$len = strlen($anons);
			$anons = ( ($len > 150) && ($len != 0) ) ? rtrim(mb_strimwidth($anons, 0, 150 - strlen('…'))) . '…' : $anons;
			if ($row['logo'] && $row['pref']=='mk') 
			{
					$pic=explode('.', $row['logo']);
					$strLogo='https://mk.mrgcdn.ru/'.$pic[0].'_w720_h540.'.$pic[1];
					$strLogo_p='https://mk.mrgcdn.ru/'.$pic[0].'.'.$pic[1];
			}
			elseif($row['logo'] && ($row['pref']=='rf' || $row['pref']=='af'))
			{
				$strLogo='https://afisha7.ru'.$row['logo'];
				$strLogo_p=$strLogo;
			}
			else
			{
				$strLogo=$row['logo'];
				$strLogo_p=$strLogo;
			}
			$strLogo_p=str_replace(['films/rb_', 'films/rf_', 'films/af_', 'films/ya_'], 'films/preview/p_', $strLogo_p);
			if (substr($strLogo, 0, 8)!='https://') $strLogo='https://afisha7.ru'.$strLogo;
			if (substr($strLogo_p, 0, 8)!='https://') $strLogo_p='https://afisha7.ru'.$strLogo_p;
			$res[]=['id'=>$row['id'], 'idfull'=>$row['idfull'], 'pref'=>$row['pref'],'cat_id'=>$row['cat_id'],'loc_id'=>$row['loc_id'], 'name'=>$row['name'], 'date_start'=>$row['date_start'], 'date_end'=>$row['date_end'], 'logo'=>$strLogo, 'logo_p'=>$strLogo_p, 'anons'=>$anons, 'age'=>$row['age_name']];//, 'places'=>$arrPlaces];
		}
		return ['total'=>$total, 'events'=>$res, ];
	} else return NULL;
}

При реализации API постарались сократить обращение к базе данных сайта: так как консолидация актуальных данных выполняется каждый час, то необходимости в постоянном обращении к БД нет. Было принято решение сбрасывать справочники в json-файл, а при обращении пользователя анализировать дату файла.

Пример получения справочника локалей
if ($_POST['APIKey'])
{
	if (!(file_exists('locales.json')))
	{
		$arr=APIAfisha7::getLocales();
		if ($arr)
		{
			$arr['lastdate']=time()+5*60;
			$str=json_encode($arr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
			unset($arr);
			file_put_contents('locales.json', $str);
			echo $str;
		} else echo json_encode(['errors'=>"not records"], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);	
	}
	else
	{
		if($_POST['lastdate'])
		{
			$ld=$_POST['lastdate']*1;
			if ($ld>=filemtime( __DIR__.'/locales.json')) echo json_encode(['errors'=>"No updates for locales"], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); else echo file_get_contents('locales.json');
		} else echo file_get_contents('locales.json');
	}
}
else
{
	echo json_encode(['errors'=>["Not APIKey", 'Only POST request.']], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}

ВКонтакте API

Интеграция ВКонтакте API позволило автоматизировать заполнение сообществ актуальными событиями и анонсами мероприятий.

В настоящее время публикуются в 12 сообществах анонсы: утром на сегодня и вечером на завтра; киноафиша на сегодня, трейлеры новинок кино, афиша событий по категориям. Доступна ручная публикация конкретных событий.

Основные возможности API, которые используются:

  • Публикация конкретного события.

  • Публикация Wiki-страницы со списком актуальных событий.

  • Загрузка логотипов актуальных событий в каждое сообщество, зарегистрированное в системе, для использования в постах об актуальных событиях по категориям.

Прототип функции публикации фотографий в альбомах сообщества
function publicPhotoArr($arr=NULL, $cat_url='')
{
	$ret=FALSE;
	$arrRet=NULL;
	$arrLogos=NULL;
	$path=dirname(__DIR__);
	$vk=new \VK\Client\VKApiClient('5.131');
	if ($arr && $cat_url)
	{
		$arrAlbums=$this->publicGetAlbums(['group_id'=>-$vk->vkkey['groups_ids'],]);
		$album_id=$arrAlbums[$cat_url];
		if (!($album_id)) return 0;
		$i=0;
		$arrFiles=NULL;
		$retServer=$vk->photos()->getUploadServer(
				$this->accessToken,
				[
					'group_id'=>$this->vkkey['groups_ids'],
					'album_id'=>$album_id,
				]
			);
		foreach ($arr as $kk=>$row)
		{
			if (substr($row['logo'],0, 8)=='/uploads')
			{
				$logo=$row['logo'];
			}
			elseif ($row['ev_id_postfix']=='mk')
			{
				$pic=explode('.', $row['logo']);
				$fname='https://mk.mrgcdn.ru/'.$pic[0].'_w720_h540.'.$pic[1];
				$logo=\admin\helpers\Image::upload_from_url($fname, $row['ev_id_char'], 'gallery/vkpub/mk', 720);//, 1024);
			} else $logo=\admin\helpers\Image::upload_from_urlWithOutDimentions($row['logo'], $row['ev_id_char'], 'gallery/vkpub/'.$row['ev_id_postfix'], 1024);
			$i++;
			$arrFiles['file'.$i]=new \CURLFile($path.$logo);
			$arrLogos[]=[$logo, $row['ev_id_char']];
			if ($i==5)
			{
			   $ch = curl_init();
				curl_setopt($ch, CURLOPT_URL, $retServer['upload_url']);
				curl_setopt($ch, CURLOPT_POST, 1);
				curl_setopt($ch, CURLOPT_POSTFIELDS, $arrFiles);
				curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
				$result = json_decode(curl_exec($ch));
				curl_close($ch);
				sleep(3);
				$ret=$vk->photos()->save(
					$this->accessToken,
					[
						'group_id'=>$this->vkkey['groups_ids'],
						'album_id'=>$album_id,
						'server'=>$result->server,
						'aid'=>$result->aid,
						'hash'=>$result->hash,
						'photos_list'=>$result->photos_list,
					]
				);
				if ($ret)
				{
					foreach($ret as $k=>$photos) $arrRet[]=['id'=>$photos['id'], 'logo'=>$arrLogos[$k][0], 'ev_id_char'=>$arrLogos[$k][1], 'album_id'=>$album_id];
				}
				$i=0;
				$arrFiles=NULL;
				$arrLogos=NULL;
			}
		}
		if ($i>0 && $arrFiles)
		{
			$ch = curl_init();
			curl_setopt($ch, CURLOPT_URL, $retServer['upload_url']);
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $arrFiles);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			$result = json_decode(curl_exec($ch));
			curl_close($ch);
			$ret=$vk->photos()->save(
				$this->accessToken,
				[
					'group_id'=>$this->vkkey['groups_ids'],
					'album_id'=>$album_id,
					'server'=>$result->server,
					'aid'=>$result->aid,
					'hash'=>$result->hash,
					'photos_list'=>$result->photos_list,
				]
			);
			if ($ret)
			{
				foreach($ret as $k=>$photos) $arrRet[]=['id'=>$photos['id'], 'logo'=>$arrLogos[$k][0], 'ev_id_char'=>$arrLogos[$k][1], 'album_id'=>$album_id];
			}
		}
	}
	return $arrRet;
}
  • Загрузка трейлеров фильмов в плей-лист сообщества для использования в постах о новинках фильмов, планируемых к показу в городе сообщества.

Прототип функции публикации видео в альбоме сообщества
function publicVideo($arr=NULL)
{
	$ret=FALSE;
	$link='';
	$path=dirname(__DIR__).'/../public_html';
	$vk=new \VK\Client\VKApiClient('5.131');
	if ($arr)
	{
		foreach ($arr as $k=>$v) $$k=$v;
		$retAlbums=$vk->publicGetPlaylists(['group_id'=>-$vk->vkkey['groups_ids'],]);
		if ($retAlbums['items'])
		{
			$album_id=$retAlbums['items'][0]['id'];
		} else return NULL;
		$arrFiles=NULL;
		$retServer=$vk->video()->save(
				$this->accessToken,
				[
					'group_id'=>$this->vkkey['groups_ids'],
					'album_id'=>$album_id,
					'name'=>'Трейлер фильма - '.$name,
					'description'=>$anons,
					'wallpost'=>0,
					'no_comments'=>1,
					'compression'=>1,
				]
			);
		if ($retServer['upload_url'])
		{
			if (substr($trailer,0, 8)!='/uploads')
			{
				$ff=fopen($trailer, 'rb');
				$fp = fopen($path.'/video.mp4', 'w+');
				$kol=stream_copy_to_stream($ff, $fp);
				fclose($fp);
				fclose($ff);
				if ($kol) $trailer='/video.mp4'; else return NULL;
			}
			$arrFiles=['video_file'=>new \CURLFile($path.$trailer)];
			$ch = curl_init();
			curl_setopt($ch, CURLOPT_URL, $retServer['upload_url']);
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, $arrFiles);
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
			$ret = json_decode(curl_exec($ch));
			curl_close($ch);
			if ($ret->owner_id * $ret->video_id *(-1)>0)
			{
				$link="https://vk.com/video".$ret->owner_id.'_'.$ret->video_id;
			}
		}
	}
	return $link;
}
  • Публикация на стене сообществ списка актуальных событий на сегодня.

Основные сложности – ограничение на количество запросов к ВКонтакте API и «непоследовательность» в работе методов (например, метод video.save() может сохранять ролики по ссылке, но лишь с избранных источников, список которых найти не удалось, что потребовало предварительной загрузки роликов на сервер и последующую выгрузку в ВК; метод photos.save() может загружать от 1 до 5 файлов одновременно, но только при сохранении одной картинки можно задать ее описание).

Конкуренция

Начиная проект, мы прекрасно отдавали отчет о конкуренции. Практически в каждом крупном городе есть региональная афиша, а то и несколько. Основные крупные медиа-площадки:

https://www.culture.ru/ - доступны только мероприятия МинКульта РФ. Отсутствуют спортивные события. Продажа билетов через ссылку на сторонние сайты.

https://afisha.yandex.ru – доступны события, на сеансы которых продают билеты через Яндекс.Афиши.

https://kassa.rambler.ru – только мероприятия, которые продают билеты на площадке Rambler-касса.

http://www.afisha.ru/ – доступны события только по крупным городам, в основном областные центры. Отсутствуют спортивные события.

https://gorodzovet.ru – доступны события для 149 городов. Отсутствуют фильмы, спортивные события.

https://kudago.com/ – только 9 крупнейших городов России. Отсутствуют спортивные события.

https://www.kassir.ru/ – доступны мероприятия, которые сами продают билеты. Только 75 крупных городов России.

https://www.kinoafisha.info/ – только фильмы. Ограничен список населенных пунктов.

Наши преимущества

  • Охвачено более 2000 населенных пунктов.

  • Более 40 тысяч мест проведения событий.

  • Наиболее полная база событий и мероприятий, структурированная по 10 категориям.

  • Доступна онлайн покупка билетов на сайте через виджеты Rambler-кассы или Яндекс.Афиши.

  • Возможность бесплатного размещения событий.

Теги:
Хабы:
Всего голосов 4: ↑3 и ↓1 +2
Просмотры 942
Комментарии Комментарии 10