Browse Source

extract photos and videos from streaming tweets when truncated

pull/64/head
Aaron Parecki 3 years ago
parent
commit
bf4bc3a668
No known key found for this signature in database GPG Key ID: 276C2817346D6056
4 changed files with 784 additions and 22 deletions
  1. +39
    -21
      lib/XRay/Formats/Twitter.php
  2. +21
    -1
      tests/TwitterTest.php
  3. +434
    -0
      tests/data/api.twitter.com/streaming-tweet-truncated-with-photo.json
  4. +290
    -0
      tests/data/api.twitter.com/streaming-tweet-truncated-with-video.json

+ 39
- 21
lib/XRay/Formats/Twitter.php View File

@ -114,27 +114,15 @@ class Twitter extends Format {
// Photos and Videos
if(property_exists($tweet, 'extended_entities') && property_exists($tweet->extended_entities, 'media')) {
foreach($tweet->extended_entities->media as $media) {
if($media->type == 'photo') {
if(!array_key_exists('photo', $entry))
$entry['photo'] = [];
$entry['photo'][] = $media->media_url_https;
} elseif($media->type == 'video') {
if(!array_key_exists('video', $entry))
$entry['video'] = [];
// Find the highest bitrate video that is mp4
$videos = $media->video_info->variants;
$videos = array_filter($videos, function($v) {
return property_exists($v, 'bitrate') && $v->content_type == 'video/mp4';
});
if(count($videos)) {
usort($videos, function($a,$b) {
return $a->bitrate < $b->bitrate;
});
$entry['video'][] = $videos[0]->url;
}
self::extractMedia($media, $entry);
}
}
// Photos from Streaming API Tweets
if(property_exists($tweet, 'extended_tweet')) {
if(property_exists($tweet->extended_tweet, 'entities') && property_exists($tweet->extended_tweet->entities, 'media')) {
foreach($tweet->extended_tweet->entities->media as $media) {
self::extractMedia($media, $entry);
}
}
}
@ -181,6 +169,31 @@ class Twitter extends Format {
];
}
private static function extractMedia($media, &$entry) {
if($media->type == 'photo') {
if(!array_key_exists('photo', $entry))
$entry['photo'] = [];
$entry['photo'][] = $media->media_url_https;
} elseif($media->type == 'video') {
if(!array_key_exists('video', $entry))
$entry['video'] = [];
// Find the highest bitrate video that is mp4
$videos = $media->video_info->variants;
$videos = array_filter($videos, function($v) {
return property_exists($v, 'bitrate') && $v->content_type == 'video/mp4';
});
if(count($videos)) {
usort($videos, function($a,$b) {
return $a->bitrate < $b->bitrate;
});
$entry['video'][] = $videos[0]->url;
}
}
}
private static function _buildHCardFromTwitterProfile($profile) {
if(!$profile) return false;
@ -223,6 +236,11 @@ class Twitter extends Format {
if(property_exists($tweet, 'extended_tweet')) {
$text = $tweet->extended_tweet->full_text;
$text = mb_substr($text,
$tweet->extended_tweet->display_text_range[0],
$tweet->extended_tweet->display_text_range[1]-$tweet->extended_tweet->display_text_range[0],
'UTF-8');
if(property_exists($tweet->extended_tweet, 'entities')) {
$entities = $tweet->extended_tweet->entities;
}

+ 21
- 1
tests/TwitterTest.php View File

@ -208,7 +208,27 @@ class TwitterTest extends PHPUnit_Framework_TestCase {
$data = $this->parse(['url' => $url, 'body' => $json]);
$this->assertEquals("#indieweb community. Really would like to see a Micropub client for Gratitude logging and also a Mastodon poster similar to the twitter one.\nFeel like I could (maybe) rewrite previous open code to do some of this :)", $data['data']['content']['text']);
$this->assertArrayNotHasKey('html', $data['data']['content']);
$this->assertEquals('#indieweb community. Really would like to see a Micropub client for Gratitude logging and also a Mastodon poster similar to the twitter one.<br>
Feel like I could (maybe) rewrite previous open code to do some of this :)', $data['data']['content']['html']);
}
public function testStreamingTweetTruncatedWithPhoto() {
list($url, $json) = $this->loadTweet('streaming-tweet-truncated-with-photo');
$data = $this->parse(['url' => $url, 'body' => $json]);
$this->assertEquals("#MicrosoftFlow ninja-tip.\nI'm getting better at custom-connector and auth. Thanks @skillriver \nThis is OAuth2 with MSA/Live (not AzureAD) which I need to do MVP timesheets.\nStill dislike Swagger so I don't know why I bother with this. I'm just that lazy doing this manually", $data['data']['content']['text']);
$this->assertEquals(4, count($data['data']['photo']));
$this->assertEquals('https://pbs.twimg.com/media/DWZ-5UPVAAAQOWY.jpg', $data['data']['photo'][0]);
$this->assertEquals('https://pbs.twimg.com/media/DWaAhZ2UQAAIEoS.jpg', $data['data']['photo'][3]);
}
public function testStreamingTweetTruncatedWithVidoe() {
list($url, $json) = $this->loadTweet('streaming-tweet-truncated-with-video');
$data = $this->parse(['url' => $url, 'body' => $json]);
$this->assertEquals("hi @aaronpk Ends was a great job I was just talking to her about the house I think she is just talking to you about that stuff like that you don't have any idea of how to make to your job so you don't want me going back on your own to make it happen", $data['data']['content']['text']);
$this->assertEquals(1, count($data['data']['video']));
$this->assertEquals('https://video.twimg.com/ext_tw_video/965608338917548032/pu/vid/720x720/kreAfCMf-B1dLqBH.mp4', $data['data']['video'][0]);
}
}

+ 434
- 0
tests/data/api.twitter.com/streaming-tweet-truncated-with-photo.json View File

@ -0,0 +1,434 @@
{
"created_at": "Mon Feb 19 15:03:23 +0000 2018",
"id": 965602723251945473,
"id_str": "965602723251945473",
"text": "#MicrosoftFlow ninja-tip.\nI'm getting better at custom-connector and auth. Thanks @skillriver \nThis is OAuth2 with\u2026 https://t.co/pL951ynEfd",
"display_text_range": [
0,
140
],
"source": "<a href=\"http://twitter.com\" rel=\"nofollow\">Twitter Web Client</a>",
"truncated": true,
"in_reply_to_status_id": null,
"in_reply_to_status_id_str": null,
"in_reply_to_user_id": null,
"in_reply_to_user_id_str": null,
"in_reply_to_screen_name": null,
"user": {
"id": 17532000,
"id_str": "17532000",
"name": "John LIU",
"screen_name": "johnnliu",
"location": "Sydney",
"url": "http://johnliu.net",
"description": "coder \u2022 blogger \u2022 MVP Office365 & SharePoint \u2022 #MSFlow \n[work sharepointgurus..net]",
"translator_type": "none",
"protected": false,
"verified": false,
"followers_count": 1361,
"friends_count": 421,
"listed_count": 142,
"favourites_count": 8661,
"statuses_count": 13745,
"created_at": "Fri Nov 21 05:46:26 +0000 2008",
"utc_offset": 39600,
"time_zone": "Sydney",
"geo_enabled": true,
"lang": "en",
"contributors_enabled": false,
"is_translator": false,
"profile_background_color": "C0DEED",
"profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
"profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
"profile_background_tile": false,
"profile_link_color": "1DA1F2",
"profile_sidebar_border_color": "C0DEED",
"profile_sidebar_fill_color": "DDEEF6",
"profile_text_color": "333333",
"profile_use_background_image": true,
"profile_image_url": "http://pbs.twimg.com/profile_images/961904825716715520/NhPxyw2N_normal.jpg",
"profile_image_url_https": "https://pbs.twimg.com/profile_images/961904825716715520/NhPxyw2N_normal.jpg",
"profile_banner_url": "https://pbs.twimg.com/profile_banners/17532000/1432696432",
"default_profile": true,
"default_profile_image": false,
"following": null,
"follow_request_sent": null,
"notifications": null
},
"geo": null,
"coordinates": null,
"place": null,
"contributors": null,
"is_quote_status": false,
"extended_tweet": {
"full_text": "#MicrosoftFlow ninja-tip.\nI'm getting better at custom-connector and auth. Thanks @skillriver \nThis is OAuth2 with MSA/Live (not AzureAD) which I need to do MVP timesheets.\nStill dislike Swagger so I don't know why I bother with this. I'm just that lazy doing this manually https://t.co/dUDaqcQssO",
"display_text_range": [
0,
274
],
"entities": {
"hashtags": [
{
"text": "MicrosoftFlow",
"indices": [
0,
14
]
}
],
"urls": [],
"user_mentions": [
{
"screen_name": "skillriver",
"name": "Jan Vidar Elven",
"id": 550859390,
"id_str": "550859390",
"indices": [
83,
94
]
}
],
"symbols": [],
"media": [
{
"id": 965598693268193280,
"id_str": "965598693268193280",
"indices": [
275,
298
],
"media_url": "http://pbs.twimg.com/media/DWZ-5UPVAAAQOWY.jpg",
"media_url_https": "https://pbs.twimg.com/media/DWZ-5UPVAAAQOWY.jpg",
"url": "https://t.co/dUDaqcQssO",
"display_url": "pic.twitter.com/dUDaqcQssO",
"expanded_url": "https://twitter.com/johnnliu/status/965602723251945473/photo/1",
"type": "photo",
"sizes": {
"small": {
"w": 638,
"h": 680,
"resize": "fit"
},
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"medium": {
"w": 754,
"h": 804,
"resize": "fit"
},
"large": {
"w": 754,
"h": 804,
"resize": "fit"
}
}
},
{
"id": 965600129775411204,
"id_str": "965600129775411204",
"indices": [
275,
298
],
"media_url": "http://pbs.twimg.com/media/DWaAM7pVoAQuGhL.jpg",
"media_url_https": "https://pbs.twimg.com/media/DWaAM7pVoAQuGhL.jpg",
"url": "https://t.co/dUDaqcQssO",
"display_url": "pic.twitter.com/dUDaqcQssO",
"expanded_url": "https://twitter.com/johnnliu/status/965602723251945473/photo/1",
"type": "photo",
"sizes": {
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"medium": {
"w": 562,
"h": 904,
"resize": "fit"
},
"large": {
"w": 562,
"h": 904,
"resize": "fit"
},
"small": {
"w": 423,
"h": 680,
"resize": "fit"
}
}
},
{
"id": 965600383589523456,
"id_str": "965600383589523456",
"indices": [
275,
298
],
"media_url": "http://pbs.twimg.com/media/DWaAbtLVoAA2O68.jpg",
"media_url_https": "https://pbs.twimg.com/media/DWaAbtLVoAA2O68.jpg",
"url": "https://t.co/dUDaqcQssO",
"display_url": "pic.twitter.com/dUDaqcQssO",
"expanded_url": "https://twitter.com/johnnliu/status/965602723251945473/photo/1",
"type": "photo",
"sizes": {
"large": {
"w": 908,
"h": 1017,
"resize": "fit"
},
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"medium": {
"w": 908,
"h": 1017,
"resize": "fit"
},
"small": {
"w": 607,
"h": 680,
"resize": "fit"
}
}
},
{
"id": 965600481480294400,
"id_str": "965600481480294400",
"indices": [
275,
298
],
"media_url": "http://pbs.twimg.com/media/DWaAhZ2UQAAIEoS.jpg",
"media_url_https": "https://pbs.twimg.com/media/DWaAhZ2UQAAIEoS.jpg",
"url": "https://t.co/dUDaqcQssO",
"display_url": "pic.twitter.com/dUDaqcQssO",
"expanded_url": "https://twitter.com/johnnliu/status/965602723251945473/photo/1",
"type": "photo",
"sizes": {
"large": {
"w": 813,
"h": 1031,
"resize": "fit"
},
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"small": {
"w": 536,
"h": 680,
"resize": "fit"
},
"medium": {
"w": 813,
"h": 1031,
"resize": "fit"
}
}
}
]
},
"extended_entities": {
"media": [
{
"id": 965598693268193280,
"id_str": "965598693268193280",
"indices": [
275,
298
],
"media_url": "http://pbs.twimg.com/media/DWZ-5UPVAAAQOWY.jpg",
"media_url_https": "https://pbs.twimg.com/media/DWZ-5UPVAAAQOWY.jpg",
"url": "https://t.co/dUDaqcQssO",
"display_url": "pic.twitter.com/dUDaqcQssO",
"expanded_url": "https://twitter.com/johnnliu/status/965602723251945473/photo/1",
"type": "photo",
"sizes": {
"small": {
"w": 638,
"h": 680,
"resize": "fit"
},
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"medium": {
"w": 754,
"h": 804,
"resize": "fit"
},
"large": {
"w": 754,
"h": 804,
"resize": "fit"
}
}
},
{
"id": 965600129775411204,
"id_str": "965600129775411204",
"indices": [
275,
298
],
"media_url": "http://pbs.twimg.com/media/DWaAM7pVoAQuGhL.jpg",
"media_url_https": "https://pbs.twimg.com/media/DWaAM7pVoAQuGhL.jpg",
"url": "https://t.co/dUDaqcQssO",
"display_url": "pic.twitter.com/dUDaqcQssO",
"expanded_url": "https://twitter.com/johnnliu/status/965602723251945473/photo/1",
"type": "photo",
"sizes": {
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"medium": {
"w": 562,
"h": 904,
"resize": "fit"
},
"large": {
"w": 562,
"h": 904,
"resize": "fit"
},
"small": {
"w": 423,
"h": 680,
"resize": "fit"
}
}
},
{
"id": 965600383589523456,
"id_str": "965600383589523456",
"indices": [
275,
298
],
"media_url": "http://pbs.twimg.com/media/DWaAbtLVoAA2O68.jpg",
"media_url_https": "https://pbs.twimg.com/media/DWaAbtLVoAA2O68.jpg",
"url": "https://t.co/dUDaqcQssO",
"display_url": "pic.twitter.com/dUDaqcQssO",
"expanded_url": "https://twitter.com/johnnliu/status/965602723251945473/photo/1",
"type": "photo",
"sizes": {
"large": {
"w": 908,
"h": 1017,
"resize": "fit"
},
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"medium": {
"w": 908,
"h": 1017,
"resize": "fit"
},
"small": {
"w": 607,
"h": 680,
"resize": "fit"
}
}
},
{
"id": 965600481480294400,
"id_str": "965600481480294400",
"indices": [
275,
298
],
"media_url": "http://pbs.twimg.com/media/DWaAhZ2UQAAIEoS.jpg",
"media_url_https": "https://pbs.twimg.com/media/DWaAhZ2UQAAIEoS.jpg",
"url": "https://t.co/dUDaqcQssO",
"display_url": "pic.twitter.com/dUDaqcQssO",
"expanded_url": "https://twitter.com/johnnliu/status/965602723251945473/photo/1",
"type": "photo",
"sizes": {
"large": {
"w": 813,
"h": 1031,
"resize": "fit"
},
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"small": {
"w": 536,
"h": 680,
"resize": "fit"
},
"medium": {
"w": 813,
"h": 1031,
"resize": "fit"
}
}
}
]
}
},
"quote_count": 0,
"reply_count": 0,
"retweet_count": 0,
"favorite_count": 0,
"entities": {
"hashtags": [
{
"text": "MicrosoftFlow",
"indices": [
0,
14
]
}
],
"urls": [
{
"url": "https://t.co/pL951ynEfd",
"expanded_url": "https://twitter.com/i/web/status/965602723251945473",
"display_url": "twitter.com/i/web/status/9\u2026",
"indices": [
117,
140
]
}
],
"user_mentions": [
{
"screen_name": "skillriver",
"name": "Jan Vidar Elven",
"id": 550859390,
"id_str": "550859390",
"indices": [
83,
94
]
}
],
"symbols": []
},
"favorited": false,
"retweeted": false,
"possibly_sensitive": false,
"filter_level": "low",
"lang": "en",
"timestamp_ms": "1519052603911"
}

+ 290
- 0
tests/data/api.twitter.com/streaming-tweet-truncated-with-video.json View File

@ -0,0 +1,290 @@
{
"created_at": "Mon Feb 19 15:25:48 +0000 2018",
"id": 965608361797419010,
"id_str": "965608361797419010",
"text": "hi @aaronpk Ends was a great job I was just talking to her about the house I think she is just talking to you about\u2026 https://t.co/ggjjk7ZZZY",
"display_text_range": [
0,
140
],
"source": "<a href=\"http://twitter.com/download/iphone\" rel=\"nofollow\">Twitter for iPhone</a>",
"truncated": true,
"in_reply_to_status_id": null,
"in_reply_to_status_id_str": null,
"in_reply_to_user_id": null,
"in_reply_to_user_id_str": null,
"in_reply_to_screen_name": null,
"user": {
"id": 143883456,
"id_str": "143883456",
"name": "aaronpk dev",
"screen_name": "pkdev",
"location": "Portland, OR",
"url": "http://aaronpk.micro.blog/",
"description": "Dev account for testing Twitter things. Follow me here: https://twitter.com/aaronpk",
"translator_type": "none",
"protected": false,
"verified": false,
"followers_count": 1,
"friends_count": 1,
"listed_count": 0,
"favourites_count": 2,
"statuses_count": 58,
"created_at": "Fri May 14 17:47:15 +0000 2010",
"utc_offset": -28800,
"time_zone": "Pacific Time (US & Canada)",
"geo_enabled": true,
"lang": "en",
"contributors_enabled": false,
"is_translator": false,
"profile_background_color": "C0DEED",
"profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png",
"profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png",
"profile_background_tile": false,
"profile_link_color": "1DA1F2",
"profile_sidebar_border_color": "C0DEED",
"profile_sidebar_fill_color": "DDEEF6",
"profile_text_color": "333333",
"profile_use_background_image": true,
"profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg",
"profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg",
"default_profile": true,
"default_profile_image": false,
"following": null,
"follow_request_sent": null,
"notifications": null
},
"geo": null,
"coordinates": null,
"place": {
"id": "ac88a4f17a51c7fc",
"url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json",
"place_type": "city",
"name": "Portland",
"full_name": "Portland, OR",
"country_code": "US",
"country": "United States",
"bounding_box": {
"type": "Polygon",
"coordinates": [
[
[
-122.790065,
45.421863
],
[
-122.790065,
45.650941
],
[
-122.471751,
45.650941
],
[
-122.471751,
45.421863
]
]
]
},
"attributes": {}
},
"contributors": null,
"is_quote_status": false,
"extended_tweet": {
"full_text": "hi @aaronpk Ends was a great job I was just talking to her about the house I think she is just talking to you about that stuff like that you don't have any idea of how to make to your job so you don't want me going back on your own to make it happen https://t.co/9ko0nnAG7K",
"display_text_range": [
0,
249
],
"entities": {
"hashtags": [],
"urls": [],
"user_mentions": [
{
"screen_name": "aaronpk",
"name": "Aaron Parecki",
"id": 14447132,
"id_str": "14447132",
"indices": [
3,
11
]
}
],
"symbols": [],
"media": [
{
"id": 965608338917548032,
"id_str": "965608338917548032",
"indices": [
250,
273
],
"media_url": "http://pbs.twimg.com/ext_tw_video_thumb/965608338917548032/pu/img/TXwZ-AA8tSYTaZYS.jpg",
"media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/965608338917548032/pu/img/TXwZ-AA8tSYTaZYS.jpg",
"url": "https://t.co/9ko0nnAG7K",
"display_url": "pic.twitter.com/9ko0nnAG7K",
"expanded_url": "https://twitter.com/pkdev/status/965608361797419010/video/1",
"type": "video",
"sizes": {
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"small": {
"w": 680,
"h": 680,
"resize": "fit"
},
"medium": {
"w": 720,
"h": 720,
"resize": "fit"
},
"large": {
"w": 720,
"h": 720,
"resize": "fit"
}
},
"video_info": {
"aspect_ratio": [
1,
1
],
"duration_millis": 3995,
"variants": [
{
"content_type": "application/x-mpegURL",
"url": "https://video.twimg.com/ext_tw_video/965608338917548032/pu/pl/KAdZKmD5lymHTt7A.m3u8"
},
{
"bitrate": 1280000,
"content_type": "video/mp4",
"url": "https://video.twimg.com/ext_tw_video/965608338917548032/pu/vid/720x720/kreAfCMf-B1dLqBH.mp4"
},
{
"bitrate": 256000,
"content_type": "video/mp4",
"url": "https://video.twimg.com/ext_tw_video/965608338917548032/pu/vid/240x240/5ZfV0xsJ4NmRKkcM.mp4"
},
{
"bitrate": 832000,
"content_type": "video/mp4",
"url": "https://video.twimg.com/ext_tw_video/965608338917548032/pu/vid/480x480/EYfnabEG0Uno0Azc.mp4"
}
]
}
}
]
},
"extended_entities": {
"media": [
{
"id": 965608338917548032,
"id_str": "965608338917548032",
"indices": [
250,
273
],
"media_url": "http://pbs.twimg.com/ext_tw_video_thumb/965608338917548032/pu/img/TXwZ-AA8tSYTaZYS.jpg",
"media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/965608338917548032/pu/img/TXwZ-AA8tSYTaZYS.jpg",
"url": "https://t.co/9ko0nnAG7K",
"display_url": "pic.twitter.com/9ko0nnAG7K",
"expanded_url": "https://twitter.com/pkdev/status/965608361797419010/video/1",
"type": "video",
"sizes": {
"thumb": {
"w": 150,
"h": 150,
"resize": "crop"
},
"small": {
"w": 680,
"h": 680,
"resize": "fit"
},
"medium": {
"w": 720,
"h": 720,
"resize": "fit"
},
"large": {
"w": 720,
"h": 720,
"resize": "fit"
}
},
"video_info": {
"aspect_ratio": [
1,
1
],
"duration_millis": 3995,
"variants": [
{
"content_type": "application/x-mpegURL",
"url": "https://video.twimg.com/ext_tw_video/965608338917548032/pu/pl/KAdZKmD5lymHTt7A.m3u8"
},
{
"bitrate": 1280000,
"content_type": "video/mp4",
"url": "https://video.twimg.com/ext_tw_video/965608338917548032/pu/vid/720x720/kreAfCMf-B1dLqBH.mp4"
},
{
"bitrate": 256000,
"content_type": "video/mp4",
"url": "https://video.twimg.com/ext_tw_video/965608338917548032/pu/vid/240x240/5ZfV0xsJ4NmRKkcM.mp4"
},
{
"bitrate": 832000,
"content_type": "video/mp4",
"url": "https://video.twimg.com/ext_tw_video/965608338917548032/pu/vid/480x480/EYfnabEG0Uno0Azc.mp4"
}
]
}
}
]
}
},
"quote_count": 0,
"reply_count": 0,
"retweet_count": 0,
"favorite_count": 0,
"entities": {
"hashtags": [],
"urls": [
{
"url": "https://t.co/ggjjk7ZZZY",
"expanded_url": "https://twitter.com/i/web/status/965608361797419010",
"display_url": "twitter.com/i/web/status/9\u2026",
"indices": [
117,
140
]
}
],
"user_mentions": [
{
"screen_name": "aaronpk",
"name": "Aaron Parecki",
"id": 14447132,
"id_str": "14447132",
"indices": [
3,
11
]
}
],
"symbols": []
},
"favorited": false,
"retweeted": false,
"possibly_sensitive": false,
"filter_level": "low",
"lang": "en",
"timestamp_ms": "1519053948245"
}

Loading…
Cancel
Save