feed2gram 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -1
- data/README.md +9 -4
- data/lib/feed2gram/parses_entries.rb +39 -3
- data/lib/feed2gram/publishes_posts.rb +69 -23
- data/lib/feed2gram/version.rb +1 -1
- data/lib/feed2gram.rb +9 -5
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 186b11421ce39cc03f041f010e695fbd295edb0631235d80545e42928fe6edca
|
4
|
+
data.tar.gz: 688601d45092f6f1c57c9871af2b337c7df93f441ccefc6b787e01169a8bd236
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4682d5d13f8443ebb0cdc853394fb9e8a0c8677a0e4850daaa0df8feb54d9fcc73cc64eb5c41368dd14c958e8a5be25fe50e2c8a006602d66751161c411c9e13
|
7
|
+
data.tar.gz: 7ca4d2096fa0b0b60df85c0f902f312ab27083a2ec9b7b0590951315370b3dd74fee02c92781ebdc7347945a5dc7142ac9f1204cbfe468125bd523c4b5509964
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,15 @@
|
|
1
|
-
## [
|
1
|
+
## [1.1.0]
|
2
|
+
|
3
|
+
* Add support for videos and stories, including:
|
4
|
+
* single-video posts (which post as reels), by setting `data-media-type=video`
|
5
|
+
attribute on a feed entry's `<figure>`'s only `<img>` child
|
6
|
+
* single-image and single-video stories, by setting `data-post-type=stories`
|
7
|
+
attribute on a feed entry's `<figure>` element
|
8
|
+
* carousels that contain videos and photos by setting `data-media-type=video`
|
9
|
+
attribute on each `<img>` tag that contains a video
|
10
|
+
* Print much more granular feedback when publishing and in verbose mode
|
11
|
+
* When all posts are filtered out from the cache, say so (when verbose) and
|
12
|
+
don't update the cache file needlessly
|
2
13
|
|
3
14
|
## [1.0.0]
|
4
15
|
|
data/README.md
CHANGED
@@ -102,13 +102,18 @@ feed2gram uses the first `<figure>` element to generate each Instagram post. Tha
|
|
102
102
|
|
103
103
|
Some things to keep in mind:
|
104
104
|
|
105
|
+
* A `<figure>` may specify a `data-post-type` with a value of `reels`, `stories`, or `post` (if unspecified, the type defaults to `post`)
|
106
|
+
* If `data-post-type` is set to `stories` or `reels`, exactly one image or video must be included. If `post`, then multiple (up to ten) images and videos can be included and will publish as a carousel post
|
107
|
+
* Posting stories (i.e. `<figure data-post-type="stories">`) requires a _business_ account, not a creator one (in which case a, "the user is not an Instagram Business," error will be returned)
|
105
108
|
* If one `<img>` tag is present, a single photo post will be created. If there are more, a [carousel post](https://developers.facebook.com/docs/instagram-api/guides/content-publishing/#carousel-posts) will be created
|
106
109
|
* Because Facebook's servers actually _download your image_ as opposed to receiving them as uploads via the API, every `<img>` tag's `src` attribute must be set to a publicly-reachable, fully-qualified URL
|
107
|
-
*
|
108
|
-
* Images must be standard-issue JPEGs, or else posting will fail
|
110
|
+
* To post videos, stories, or reels, set the `data-media-type` attribute on the `<img>` tag to `video` or `image` (a media type of `image` will be assumed by default if left unspecified). Note that while `image` and `video` media may be interspersed throughout a carousel
|
109
111
|
* For carousel posts, the aspect ratio of the first image determines the aspect ratio of the rest, so be mindful of how you order the images based on how you want them to appear in the app
|
110
112
|
* Only one caption will be published, regardless of whether it's a single photo post or a carousel
|
111
113
|
* The caption limit is 2200 characters, so feed2gram will truncate it if necessary
|
114
|
+
* The API is pretty strict about media file formats, too, so you may wish to preprocess images and videos to avoid errors in processing:
|
115
|
+
* Images can't be more than 8MB and must be standard-issue JPEGs
|
116
|
+
* Videos are even stricter (best to just [read the docs](https://developers.facebook.com/docs/instagram-api/reference/ig-user/media#creating), including this bit on [reels](https://developers.facebook.com/docs/video-api/guides/reels-publishing)). Videos that appear in carousels seem to have additional no-longer-documented restrictions (in my testing, 9:16 videos routinely failed but 16:9, 1:1, 4:3, and 3:4 succeeded)
|
112
117
|
|
113
118
|
Here's an example `<entry>` from my blog feed:
|
114
119
|
|
@@ -207,7 +212,7 @@ Look at your cache file (by default, `feed2gram.cache.yml`) and you should see
|
|
207
212
|
all the Atom feed entry URLs that succeeded, failed, or were (by the `--populate-cache` option) skipped. If you don't see the error in the log, try
|
208
213
|
removing the relevant URL from the cache and running `feed2gram` again.
|
209
214
|
|
210
|
-
### What are the valid aspect ratios?
|
215
|
+
### What are the valid aspect ratios for images?
|
211
216
|
|
212
217
|
If you're seeing an embedded API error like this one:
|
213
218
|
|
@@ -216,6 +221,6 @@ The submitted image with aspect ratio ('719/194',) cannot be published. Please s
|
|
216
221
|
```
|
217
222
|
|
218
223
|
It means your photo is too avant garde for a mainstream normie platform like
|
219
|
-
Instagram. Make sure all images' aspect
|
224
|
+
Instagram. Make sure all images' aspect ratios are between 4:5 and 1.91:1 or
|
220
225
|
else the post will fail.
|
221
226
|
|
@@ -2,20 +2,56 @@ require "nokogiri"
|
|
2
2
|
require "open-uri"
|
3
3
|
|
4
4
|
module Feed2Gram
|
5
|
-
|
5
|
+
Media = Struct.new(:media_type, :url, keyword_init: true) do
|
6
|
+
def video?
|
7
|
+
media_type == "VIDEO"
|
8
|
+
end
|
9
|
+
end
|
10
|
+
Post = Struct.new(:media_type, :url, :medias, :caption, keyword_init: true)
|
6
11
|
|
7
12
|
class ParsesEntries
|
8
13
|
def parse(feed_url)
|
9
14
|
feed = Nokogiri::XML(URI.parse(feed_url).open)
|
10
15
|
feed.xpath("//*:entry").map { |entry|
|
11
16
|
html = Nokogiri::HTML(entry.xpath("*:content[1]").text)
|
17
|
+
medias = html.xpath("//figure[1]/img").map { |img|
|
18
|
+
Media.new(
|
19
|
+
media_type: (img["data-media-type"] || "image").upcase,
|
20
|
+
url: img["src"]
|
21
|
+
)
|
22
|
+
}
|
12
23
|
|
13
24
|
Post.new(
|
25
|
+
media_type: determine_post_media_type(html, medias),
|
14
26
|
url: entry.xpath("*:id[1]").text,
|
15
|
-
|
27
|
+
medias: medias,
|
16
28
|
caption: html.xpath("//figure[1]/figcaption").text.strip
|
17
29
|
)
|
18
|
-
}.
|
30
|
+
}.select { |post|
|
31
|
+
if post.medias.empty?
|
32
|
+
warn "Skipping post with no <img> tag: #{post.url}"
|
33
|
+
elsif ["STORIES", "REELS"].include?(post.media_type) && post.medias.size > 1
|
34
|
+
warn "Skipping #{post.media_type.downcase} with more than one <img> tag (only one allowed): #{post.url}"
|
35
|
+
else
|
36
|
+
true
|
37
|
+
end
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def determine_post_media_type(html, medias)
|
44
|
+
post_type = html.at("//figure[1]")["data-post-type"]&.upcase
|
45
|
+
if ["STORIES", "REELS"].include?(post_type)
|
46
|
+
post_type
|
47
|
+
elsif medias.size > 1
|
48
|
+
"CAROUSEL"
|
49
|
+
elsif medias.first.media_type == "VIDEO"
|
50
|
+
# The VIDEO value for media_type is deprecated outside carousel items. Use the REELS media type to publish a video to your Instagram feed. Please visit https://developers.facebook.com/docs/instagram-api/reference/ig-user/media#creating to publish a video.
|
51
|
+
"REELS"
|
52
|
+
else
|
53
|
+
"IMAGE"
|
54
|
+
end
|
19
55
|
end
|
20
56
|
end
|
21
57
|
end
|
@@ -9,12 +9,12 @@ module Feed2Gram
|
|
9
9
|
# reverse to post oldest first (most Atom feeds are reverse-chronological)
|
10
10
|
posts.reverse.take(post_limit).map { |post|
|
11
11
|
begin
|
12
|
-
if post.
|
13
|
-
puts "Publishing
|
14
|
-
|
12
|
+
if post.medias.size == 1
|
13
|
+
puts "Publishing #{post.media_type.downcase} for: #{post.url}" if options.verbose
|
14
|
+
publish_single_media(post, config, options)
|
15
15
|
else
|
16
|
-
puts "Publishing carousel
|
17
|
-
publish_carousel(post, config)
|
16
|
+
puts "Publishing carousel for: #{post.url}" if options.verbose
|
17
|
+
publish_carousel(post, config, options)
|
18
18
|
end
|
19
19
|
rescue => e
|
20
20
|
warn "Failed to post #{post.url}: #{e.message}"
|
@@ -25,39 +25,85 @@ module Feed2Gram
|
|
25
25
|
|
26
26
|
private
|
27
27
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
28
|
+
def publish_single_media(post, config, options)
|
29
|
+
media = post.medias.first
|
30
|
+
|
31
|
+
puts "Creating media resource for URL - #{media.url}" if options.verbose
|
32
|
+
container_id = Http.post("/#{config.instagram_id}/media", {
|
33
|
+
:media_type => post.media_type,
|
34
|
+
:caption => post.caption,
|
35
|
+
:access_token => config.access_token,
|
36
|
+
media.video? ? :video_url : :image_url => media.url
|
37
|
+
}.compact)[:id]
|
38
|
+
|
39
|
+
if media.video?
|
40
|
+
wait_for_media_to_upload!(media.url, container_id, config, options)
|
41
|
+
end
|
42
|
+
|
43
|
+
puts "Publishing media for URL - #{media.url}" if options.verbose
|
34
44
|
Http.post("/#{config.instagram_id}/media_publish", {
|
35
|
-
creation_id:
|
45
|
+
creation_id: container_id,
|
36
46
|
access_token: config.access_token
|
37
47
|
})
|
38
48
|
Result.new(post: post, status: :posted)
|
39
49
|
end
|
40
50
|
|
41
|
-
def publish_carousel(post, config)
|
42
|
-
|
51
|
+
def publish_carousel(post, config, options)
|
52
|
+
media_containers = post.medias.take(10).map { |media|
|
53
|
+
puts "Creating media resource for URL - #{media.url}" if options.verbose
|
43
54
|
res = Http.post("/#{config.instagram_id}/media", {
|
44
|
-
|
45
|
-
|
46
|
-
access_token
|
47
|
-
|
55
|
+
:media_type => media.media_type,
|
56
|
+
:is_carousel_item => true,
|
57
|
+
:access_token => config.access_token,
|
58
|
+
media.video? ? :video_url : :image_url => media.url
|
59
|
+
}.compact)
|
48
60
|
res[:id]
|
49
61
|
}
|
50
|
-
|
62
|
+
post.medias.select(&:video?).zip(media_containers).each { |media, container_id|
|
63
|
+
wait_for_media_to_upload!(media.url, container_id, config, options)
|
64
|
+
}
|
65
|
+
|
66
|
+
puts "Creating carousel media resource for post - #{post.url}" if options.verbose
|
67
|
+
carousel_id = Http.post("/#{config.instagram_id}/media", {
|
51
68
|
caption: post.caption,
|
52
|
-
media_type:
|
53
|
-
children:
|
69
|
+
media_type: post.media_type,
|
70
|
+
children: media_containers.join(","),
|
54
71
|
access_token: config.access_token
|
55
|
-
})
|
72
|
+
})[:id]
|
73
|
+
wait_for_media_to_upload!(post.url, carousel_id, config, options)
|
74
|
+
|
75
|
+
puts "Publishing carousel media for post - #{post.url}" if options.verbose
|
56
76
|
Http.post("/#{config.instagram_id}/media_publish", {
|
57
|
-
creation_id:
|
77
|
+
creation_id: carousel_id,
|
58
78
|
access_token: config.access_token
|
59
79
|
})
|
60
80
|
Result.new(post: post, status: :posted)
|
61
81
|
end
|
82
|
+
|
83
|
+
# Good ol' loop-and-sleep. Haven't loop do'd in a while
|
84
|
+
def wait_for_media_to_upload!(url, container_id, config, options)
|
85
|
+
wait_attempts = 0
|
86
|
+
loop do
|
87
|
+
if wait_attempts > 90
|
88
|
+
warn "Giving up waiting for media to upload after waiting 120 seconds: #{url}"
|
89
|
+
break
|
90
|
+
end
|
91
|
+
|
92
|
+
res = Http.get("/#{container_id}", {
|
93
|
+
fields: "status_code",
|
94
|
+
access_token: config.access_token
|
95
|
+
})
|
96
|
+
puts "Upload status #{res[:status_code]} after #{wait_attempts + 1} check for #{url}" if options.verbose
|
97
|
+
if res[:status_code] == "FINISHED"
|
98
|
+
break
|
99
|
+
elsif res[:status_code] == "IN_PROGRESS"
|
100
|
+
wait_attempts += 1
|
101
|
+
sleep 1
|
102
|
+
else
|
103
|
+
warn "Unexpected status code (#{res[:status_code]}) uploading: #{url}"
|
104
|
+
break
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
62
108
|
end
|
63
109
|
end
|
data/lib/feed2gram/version.rb
CHANGED
data/lib/feed2gram.rb
CHANGED
@@ -26,12 +26,16 @@ module Feed2Gram
|
|
26
26
|
entries = ParsesEntries.new.parse(config.feed_url)
|
27
27
|
puts "Found #{entries.size} entries in feed" if options.verbose
|
28
28
|
posts = FiltersPosts.new.filter(entries, cache)
|
29
|
-
|
30
|
-
puts "
|
31
|
-
posts.map { |post| Result.new(post: post, status: :skipped) }
|
29
|
+
if posts.empty?
|
30
|
+
puts "No new posts to publish after filtering already-processed posts in #{options.cache_path}" if options.verbose
|
32
31
|
else
|
33
|
-
|
32
|
+
results = if options.populate_cache
|
33
|
+
puts "Populating cache, marking #{posts.size} posts as skipped" if options.verbose
|
34
|
+
posts.map { |post| Result.new(post: post, status: :skipped) }
|
35
|
+
else
|
36
|
+
PublishesPosts.new.publish(posts, config, options)
|
37
|
+
end
|
38
|
+
UpdatesCache.new.update!(cache, results, options)
|
34
39
|
end
|
35
|
-
UpdatesCache.new.update!(cache, results, options)
|
36
40
|
end
|
37
41
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feed2gram
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Justin Searls
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-11-
|
11
|
+
date: 2023-11-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -73,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: '0'
|
75
75
|
requirements: []
|
76
|
-
rubygems_version: 3.4.
|
76
|
+
rubygems_version: 3.4.21
|
77
77
|
signing_key:
|
78
78
|
specification_version: 4
|
79
79
|
summary: Reads an Atom feed and posts its entries to Instagram
|