tape-chr 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +228 -0
- data/LICENSE.md +21 -0
- data/README.md +47 -0
- data/Rakefile +1 -0
- data/app/assets/javascripts/tape/posts_header.coffee +31 -0
- data/app/assets/javascripts/tape/posts_item.coffee +75 -0
- data/app/assets/javascripts/tape/website_view.coffee +105 -0
- data/app/assets/javascripts/tape.coffee +67 -0
- data/app/assets/stylesheets/tape/posts.scss +39 -0
- data/app/assets/stylesheets/tape/subscriptions.scss +76 -0
- data/app/assets/stylesheets/tape.scss +99 -0
- data/app/controllers/admin/tape_posts_controller.rb +9 -0
- data/app/controllers/admin/tape_subscriptions_controller.rb +17 -0
- data/app/models/tape_channel.rb +20 -0
- data/app/models/tape_post.rb +70 -0
- data/app/models/tape_subscription.rb +40 -0
- data/app/services/tape_discovery_service.rb +145 -0
- data/app/services/tape_subscriptions_service.rb +81 -0
- data/lib/tape/engine.rb +5 -0
- data/lib/tape/routing.rb +11 -0
- data/lib/tape/version.rb +3 -0
- data/lib/tape.rb +13 -0
- data/tape-chr.gemspec +26 -0
- metadata +166 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
.list.subscriptions .header {
|
2
|
+
background-color: $bg-color;
|
3
|
+
}
|
4
|
+
|
5
|
+
.subscriptions {
|
6
|
+
background-color: $bg-color;
|
7
|
+
|
8
|
+
.item-title {
|
9
|
+
font-size : .9em;
|
10
|
+
}
|
11
|
+
|
12
|
+
.item.has-thumbnail.has-subtitle {
|
13
|
+
padding-left : 3.75em;
|
14
|
+
}
|
15
|
+
|
16
|
+
.item-thumbnail {
|
17
|
+
height : 1.8em;
|
18
|
+
width : 1.8em;
|
19
|
+
left : 1em;
|
20
|
+
top : 1.1em;
|
21
|
+
}
|
22
|
+
|
23
|
+
.item-thumbnail img {
|
24
|
+
border-radius : 0;
|
25
|
+
}
|
26
|
+
}
|
27
|
+
|
28
|
+
.subscriptions .input-channels {
|
29
|
+
.nested-form-new,
|
30
|
+
.nested-form-delete {
|
31
|
+
display : none;
|
32
|
+
}
|
33
|
+
|
34
|
+
ul {
|
35
|
+
margin-top : .45em;
|
36
|
+
}
|
37
|
+
|
38
|
+
ul li {
|
39
|
+
margin-top : .5em;
|
40
|
+
border : none;
|
41
|
+
}
|
42
|
+
|
43
|
+
.input-switch {
|
44
|
+
padding : 0;
|
45
|
+
|
46
|
+
.label {
|
47
|
+
padding-right : 4em;
|
48
|
+
line-height : 1.2;
|
49
|
+
}
|
50
|
+
|
51
|
+
.switch {
|
52
|
+
top : 1px;
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
& > .label {
|
57
|
+
display : none;
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
.tape-channel-title {
|
62
|
+
@include ellipsis;
|
63
|
+
font-size : .9em;
|
64
|
+
color : $base-font-color;
|
65
|
+
}
|
66
|
+
|
67
|
+
.tape-channel-url {
|
68
|
+
@include position(relative, -4px null null null);
|
69
|
+
@include ellipsis;
|
70
|
+
font-size : .8em;
|
71
|
+
}
|
72
|
+
|
73
|
+
/* Tablet ------------------------------------------------------------------ */
|
74
|
+
@media #{$desktop} {
|
75
|
+
.subscriptions .header .close { @include header-back-icon; }
|
76
|
+
}
|
@@ -0,0 +1,99 @@
|
|
1
|
+
@import "tape/posts";
|
2
|
+
@import "tape/subscriptions";
|
3
|
+
|
4
|
+
.list.tape {
|
5
|
+
width : inherit;
|
6
|
+
|
7
|
+
.header .title {
|
8
|
+
display : none;
|
9
|
+
}
|
10
|
+
}
|
11
|
+
|
12
|
+
.list.tape .item.is-folder {
|
13
|
+
display : none;
|
14
|
+
}
|
15
|
+
|
16
|
+
.tape-header-subscriptions {
|
17
|
+
@include header-icon-base;
|
18
|
+
@include position(absolute, null 0 null null);
|
19
|
+
}
|
20
|
+
|
21
|
+
.tape-header-subscriptions + .search {
|
22
|
+
right : 2.5em;
|
23
|
+
}
|
24
|
+
|
25
|
+
.list-search .tape-header-subscriptions + .search {
|
26
|
+
right : 0;
|
27
|
+
}
|
28
|
+
|
29
|
+
.tape-refresh-posts {
|
30
|
+
@include header-icon-base;
|
31
|
+
@include position(absolute, null null null 2.5em);
|
32
|
+
}
|
33
|
+
|
34
|
+
.show-spinner .tape-refresh-posts {
|
35
|
+
visibility: hidden;
|
36
|
+
}
|
37
|
+
|
38
|
+
.tape-header-next {
|
39
|
+
@include position(absolute, 0 1em null null);
|
40
|
+
font-weight : $medium;
|
41
|
+
}
|
42
|
+
|
43
|
+
/* Tablet ------------------------------------------------------------------ */
|
44
|
+
@media #{$tablet} {
|
45
|
+
.tape-refresh-posts {
|
46
|
+
left : 0;
|
47
|
+
}
|
48
|
+
|
49
|
+
.subscriptions .header .title {
|
50
|
+
line-height : 2.95;
|
51
|
+
font-size : .9em;
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
/* Desktop ----------------------------------------------------------------- */
|
56
|
+
@media #{$desktop} {
|
57
|
+
.list.tape {
|
58
|
+
background-color : white;
|
59
|
+
right : 22em;
|
60
|
+
|
61
|
+
.header {
|
62
|
+
background-color : white;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
.tape .view.subscriptions,
|
67
|
+
.tape .list.subscriptions {
|
68
|
+
width : 22em;
|
69
|
+
right : 0;
|
70
|
+
left : initial;
|
71
|
+
}
|
72
|
+
|
73
|
+
.tape-post {
|
74
|
+
max-width : 580px;
|
75
|
+
padding : 2em 1em;
|
76
|
+
margin : -1px auto 0;
|
77
|
+
|
78
|
+
&:after {
|
79
|
+
right : 1em;
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
.tape-header-subscriptions {
|
84
|
+
display : none;
|
85
|
+
}
|
86
|
+
|
87
|
+
.tape .list.subscriptions .back {
|
88
|
+
display : none;
|
89
|
+
}
|
90
|
+
|
91
|
+
.tape-header-subscriptions + .search {
|
92
|
+
right : 0;
|
93
|
+
}
|
94
|
+
|
95
|
+
.tape-refresh-posts {
|
96
|
+
right : 2.5em;
|
97
|
+
left : initial;
|
98
|
+
}
|
99
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Admin::TapeSubscriptionsController < Admin::BaseController
|
2
|
+
mongosteen
|
3
|
+
|
4
|
+
def new
|
5
|
+
url = params['url']
|
6
|
+
subscription = TapeDiscoveryService.new(url).fetch_subscription
|
7
|
+
|
8
|
+
if not subscription
|
9
|
+
render nothing: true
|
10
|
+
|
11
|
+
else
|
12
|
+
render json: subscription
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class TapeChannel
|
2
|
+
include Mongoid::Document
|
3
|
+
include Ants::Id
|
4
|
+
|
5
|
+
|
6
|
+
## Attributes
|
7
|
+
field :title
|
8
|
+
field :url
|
9
|
+
field :active, type: Boolean, default: true
|
10
|
+
|
11
|
+
|
12
|
+
## Validators
|
13
|
+
validates_presence_of :title
|
14
|
+
validates_presence_of :url
|
15
|
+
|
16
|
+
|
17
|
+
## Relations
|
18
|
+
embedded_in :subscription, class_name: 'TapeSubscription'
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class TapePost
|
2
|
+
include Mongoid::Document
|
3
|
+
include Mongoid::Timestamps
|
4
|
+
include Mongoid::Search
|
5
|
+
include Ants::Id
|
6
|
+
|
7
|
+
|
8
|
+
## Attributes
|
9
|
+
field :entry_id
|
10
|
+
field :title
|
11
|
+
field :url
|
12
|
+
field :summary
|
13
|
+
field :image_url, default: ''
|
14
|
+
field :published_at, type: DateTime
|
15
|
+
field :subscription_title
|
16
|
+
field :subscription_icon_url, default: ''
|
17
|
+
field :channel_title
|
18
|
+
field :channel_url
|
19
|
+
|
20
|
+
|
21
|
+
## Validations
|
22
|
+
validates_uniqueness_of :entry_id
|
23
|
+
|
24
|
+
|
25
|
+
## Search
|
26
|
+
search_in :title, :subscription_title
|
27
|
+
|
28
|
+
|
29
|
+
## Scopes
|
30
|
+
default_scope -> { desc(:published_at) }
|
31
|
+
|
32
|
+
|
33
|
+
## Relations
|
34
|
+
belongs_to :subscription, class_name: 'TapeSubscription'
|
35
|
+
|
36
|
+
|
37
|
+
## Helpers
|
38
|
+
def _list_item_title
|
39
|
+
title
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
def _list_item_subtitle
|
44
|
+
if published_at > DateTime.now - 6.hours
|
45
|
+
ActionController::Base.helpers.time_ago_in_words(published_at) + ' ago'
|
46
|
+
|
47
|
+
elsif published_at.today?
|
48
|
+
published_at.strftime("%H:%M")
|
49
|
+
|
50
|
+
elsif published_at.year == DateTime.now.year
|
51
|
+
published_at.strftime("%d %b at %H:%M")
|
52
|
+
|
53
|
+
else
|
54
|
+
published_at.strftime("%d %b %Y at %H:%M")
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
def _list_item_thumbnail
|
61
|
+
image_url
|
62
|
+
# if ! image_url.empty?
|
63
|
+
# width = 480
|
64
|
+
# height = 260
|
65
|
+
# quality = 70
|
66
|
+
# return "http://www.you-tracker.com/API/ImageResizer?ytw=#{ width }&yth=#{ height }&ytaspect=true&ytquality=#{ quality }&ytimageurl=#{ image_url }"
|
67
|
+
# end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class TapeSubscription
|
2
|
+
include Mongoid::Document
|
3
|
+
include Mongoid::Timestamps
|
4
|
+
include Mongoid::Search
|
5
|
+
include Ants::Id
|
6
|
+
|
7
|
+
## Attributes
|
8
|
+
field :title
|
9
|
+
field :website_url
|
10
|
+
field :website_icon_url, default: ''
|
11
|
+
|
12
|
+
## Validators
|
13
|
+
validates_presence_of :title
|
14
|
+
validates_presence_of :website_url
|
15
|
+
|
16
|
+
## Search
|
17
|
+
search_in :title
|
18
|
+
|
19
|
+
## Relations
|
20
|
+
has_many :posts, class_name: 'TapePost', dependent: :destroy
|
21
|
+
embeds_many :channels, class_name: 'TapeChannel'
|
22
|
+
accepts_nested_attributes_for :channels
|
23
|
+
|
24
|
+
## Helpers
|
25
|
+
def _list_item_title
|
26
|
+
title
|
27
|
+
end
|
28
|
+
|
29
|
+
# def _list_item_subtitle
|
30
|
+
# website_url
|
31
|
+
# end
|
32
|
+
|
33
|
+
# def _list_item_thumbnail
|
34
|
+
# website_icon_url
|
35
|
+
# end
|
36
|
+
|
37
|
+
def active_channels
|
38
|
+
channels.select { |c| c.active }
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
class TapeDiscoveryService
|
2
|
+
|
3
|
+
CONTENT_TYPES = [
|
4
|
+
'application/x.atom+xml',
|
5
|
+
'application/atom+xml',
|
6
|
+
'application/xml',
|
7
|
+
'text/xml',
|
8
|
+
'application/rss+xml',
|
9
|
+
'application/rdf+xml',
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
def initialize(url)
|
13
|
+
@url_uri = URI.parse(url)
|
14
|
+
url = "#{ @url_uri.scheme or 'http' }://#{ @url_uri.host }#{ @url_uri.path }"
|
15
|
+
url << "?#{ @url_uri.query }" if @url_uri.query
|
16
|
+
|
17
|
+
begin
|
18
|
+
html = Nokogiri::HTML(open(url))
|
19
|
+
|
20
|
+
rescue RuntimeError
|
21
|
+
if url.start_with? 'http://'
|
22
|
+
url.gsub!('http://', 'https://')
|
23
|
+
html = Nokogiri::HTML(open(url))
|
24
|
+
|
25
|
+
# TODO: add anther option for https://www for naked subdomain
|
26
|
+
else
|
27
|
+
url = ''
|
28
|
+
html = ''
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
rescue SocketError
|
33
|
+
ap "Bad server address: #{ url }"
|
34
|
+
url = ''
|
35
|
+
html = ''
|
36
|
+
|
37
|
+
rescue Errno::ECONNREFUSED
|
38
|
+
ap "Scheme is not supported: #{ url }"
|
39
|
+
url = ''
|
40
|
+
html = ''
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
@url = url
|
45
|
+
@html = html
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def fetch_subscription
|
50
|
+
if @url.empty?
|
51
|
+
return nil
|
52
|
+
end
|
53
|
+
|
54
|
+
icon_url = fetch_icon()
|
55
|
+
|
56
|
+
ap icon_url
|
57
|
+
|
58
|
+
@subscription = TapeSubscription.new({
|
59
|
+
title: title,
|
60
|
+
website_url: @url,
|
61
|
+
website_icon_url: icon_url
|
62
|
+
})
|
63
|
+
|
64
|
+
add_feeds()
|
65
|
+
|
66
|
+
return @subscription
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def fetch_icon
|
71
|
+
api_url = "http://icons.better-idea.org/allicons.json?pretty=true&url="
|
72
|
+
icon_url = ''
|
73
|
+
|
74
|
+
json = Net::HTTP.get(URI(api_url + @url))
|
75
|
+
data = JSON.parse(json)
|
76
|
+
icon = data['icons'].first
|
77
|
+
|
78
|
+
if icon
|
79
|
+
icon_url = icon.fetch('url', '')
|
80
|
+
end
|
81
|
+
|
82
|
+
return icon_url
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def title
|
89
|
+
(@html.css('title').text.presence || 'No Title') .sanitize
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
def add_feeds
|
94
|
+
@feed_urls = []
|
95
|
+
doc = @html
|
96
|
+
|
97
|
+
if doc.at('base') and doc.at('base')['href']
|
98
|
+
@base_uri = doc.at('base')['href']
|
99
|
+
else
|
100
|
+
@base_uri = nil
|
101
|
+
end
|
102
|
+
|
103
|
+
(doc/'atom:link').each do |l|
|
104
|
+
next unless l['rel']
|
105
|
+
if l['type'] and CONTENT_TYPES.include?(l['type'].downcase.strip) and l['rel'].downcase == 'self'
|
106
|
+
add_feed(l['title'], l['href'], @url, @base_uri)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
(doc/'link').each do |l|
|
111
|
+
next unless l['rel']
|
112
|
+
if l['type'] and CONTENT_TYPES.include?(l['type'].downcase.strip) and (l['rel'].downcase =~ /alternate/i or l['rel'] == 'service.feed')
|
113
|
+
add_feed(l['title'], l['href'], @url, @base_uri)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
def add_feed(feed_title, feed_url, orig_url, base_uri = nil)
|
120
|
+
title = feed_title.presence || 'Channel'
|
121
|
+
url = feed_url.sub(/^feed:/, '').strip
|
122
|
+
|
123
|
+
if base_uri
|
124
|
+
url = URI.parse(base_uri).merge(feed_url).to_s
|
125
|
+
end
|
126
|
+
|
127
|
+
begin
|
128
|
+
uri = URI.parse(url)
|
129
|
+
rescue
|
130
|
+
puts "Error with `#{url}'"
|
131
|
+
exit 1
|
132
|
+
end
|
133
|
+
|
134
|
+
unless uri.absolute?
|
135
|
+
orig = URI.parse(orig_url)
|
136
|
+
url = orig.merge(url).to_s
|
137
|
+
end
|
138
|
+
|
139
|
+
if ! @feed_urls.include?(url)
|
140
|
+
@feed_urls << url
|
141
|
+
@subscription.channels.new(title: title, url: url)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class TapeSubscriptionsService
|
2
|
+
|
3
|
+
def fetch
|
4
|
+
@subscriptions = TapeSubscription.all
|
5
|
+
@subscriptions.each { |s| fetch_subscription_posts(s) }
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def fetch_subscription_posts(subscription)
|
12
|
+
ap subscription.title
|
13
|
+
|
14
|
+
subscription.active_channels.each do |channel|
|
15
|
+
ap channel.url
|
16
|
+
begin
|
17
|
+
feedjira_feed = Feedjira::Feed.fetch_and_parse channel.url
|
18
|
+
rescue # Feedjira::NoParserAvailable
|
19
|
+
ap "Bad feed URL: #{ channel.url } — #{ channel.title }"
|
20
|
+
else
|
21
|
+
create_new_posts(subscription, channel, feedjira_feed)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
def create_new_posts(subscription, channel, feedjira_feed)
|
28
|
+
feedjira_feed.entries.each do |e|
|
29
|
+
ap " - #{ e.title }"
|
30
|
+
|
31
|
+
title = textify(e.title)
|
32
|
+
summary = normalize(e.summary)
|
33
|
+
|
34
|
+
subscription.posts.create({ entry_id: e.entry_id,
|
35
|
+
title: title,
|
36
|
+
url: e.url,
|
37
|
+
summary: summary,
|
38
|
+
image_url: '', #post_image_url(e.url),
|
39
|
+
published_at: e.published,
|
40
|
+
subscription_title: subscription.title,
|
41
|
+
subscription_icon_url: subscription.website_icon_url,
|
42
|
+
channel_title: channel.title,
|
43
|
+
channel_url: channel.url })
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def normalize(string)
|
49
|
+
text = textify(string)
|
50
|
+
ActionController::Base.helpers.truncate(text, length: 240)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def textify(string)
|
55
|
+
if string.nil?
|
56
|
+
''
|
57
|
+
else
|
58
|
+
ActionController::Base.helpers.strip_tags(string.sanitize).gsub("\n", ' ').strip
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def post_image_url(post_url)
|
63
|
+
image_url = ''
|
64
|
+
|
65
|
+
doc = Nokogiri::HTML(open(post_url))
|
66
|
+
|
67
|
+
(doc/'meta').each do |m|
|
68
|
+
next unless m['property']
|
69
|
+
if m['property'].downcase.strip == 'og:image'
|
70
|
+
image_url = m['content']
|
71
|
+
break
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if ! image_url.empty?
|
76
|
+
return image_url
|
77
|
+
end
|
78
|
+
|
79
|
+
return ''
|
80
|
+
end
|
81
|
+
end
|
data/lib/tape/engine.rb
ADDED
data/lib/tape/routing.rb
ADDED
data/lib/tape/version.rb
ADDED
data/lib/tape.rb
ADDED
data/tape-chr.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "tape/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "tape-chr"
|
7
|
+
s.version = Tape::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Alexander Kravets"]
|
10
|
+
s.email = "alex@slatestudio.com"
|
11
|
+
s.summary = "RSS reader for Character based website"
|
12
|
+
s.homepage = "https://github.com/alexkravets/tape"
|
13
|
+
s.license = "MIT"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
|
18
|
+
s.add_dependency "chr", ">= 0.4.0"
|
19
|
+
s.add_dependency "ants", ">= 0.2.0"
|
20
|
+
s.add_dependency "mongosteen", ">= 0.1.8"
|
21
|
+
s.add_dependency "nokogiri"
|
22
|
+
s.add_dependency "feedjira"
|
23
|
+
|
24
|
+
s.add_development_dependency "bundler", "~> 1.9"
|
25
|
+
s.add_development_dependency "rake", "~> 10.0"
|
26
|
+
end
|