tape-chr 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|