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.
@@ -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