tape-chr 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,9 @@
1
+ class Admin::TapePostsController < Admin::BaseController
2
+ mongosteen
3
+
4
+ def new
5
+ TapeSubscriptionsService.new.fetch()
6
+ render nothing: true
7
+ end
8
+
9
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ module Tape
2
+ class Engine < Rails::Engine
3
+ # auto wire
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+ def mount_tape_subscriptions_crud
4
+ resources :tape_subscriptions, controller: 'tape_subscriptions'
5
+ end
6
+
7
+ def mount_tape_posts_crud
8
+ resources :tape_posts, controller: 'tape_posts'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Tape
2
+ VERSION = "0.1.8"
3
+ end
data/lib/tape.rb ADDED
@@ -0,0 +1,13 @@
1
+ require 'chr'
2
+ require 'ants'
3
+ require 'mongosteen'
4
+ require 'nokogiri'
5
+ require 'open-uri'
6
+ require 'feedjira'
7
+
8
+ module Tape
9
+ class Engine < ::Rails::Engine
10
+ require 'tape/engine'
11
+ require 'tape/routing'
12
+ end
13
+ end
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