simple_pvr 1.0.0 → 1.1.0

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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +1 -4
  5. data/Gemfile.lock +72 -61
  6. data/README.md +51 -33
  7. data/bin/pvr_server +14 -6
  8. data/changelog.txt +11 -0
  9. data/development_server +14 -6
  10. data/features/channel_overview.feature +1 -1
  11. data/features/recordings.feature +30 -0
  12. data/features/step_definitions/pvr_steps.rb +34 -0
  13. data/features/support/env.rb +1 -2
  14. data/features/support/paths.rb +2 -0
  15. data/lib/simple_pvr.rb +2 -0
  16. data/lib/simple_pvr/model/database_initializer.rb +8 -0
  17. data/lib/simple_pvr/model/programme.rb +12 -2
  18. data/lib/simple_pvr/model/programme_actor.rb +14 -0
  19. data/lib/simple_pvr/model/programme_category.rb +14 -0
  20. data/lib/simple_pvr/model/programme_director.rb +13 -0
  21. data/lib/simple_pvr/model/programme_presenter.rb +13 -0
  22. data/lib/simple_pvr/model/recording.rb +12 -0
  23. data/lib/simple_pvr/programme_icon_fetcher.rb +15 -0
  24. data/lib/simple_pvr/pvr_initializer.rb +4 -4
  25. data/lib/simple_pvr/recorder.rb +4 -3
  26. data/lib/simple_pvr/recording_manager.rb +44 -24
  27. data/lib/simple_pvr/recording_planner.rb +3 -3
  28. data/lib/simple_pvr/scheduler.rb +12 -4
  29. data/lib/simple_pvr/server/base_controller.rb +12 -13
  30. data/lib/simple_pvr/server/channels_controller.rb +5 -5
  31. data/lib/simple_pvr/server/programmes_controller.rb +2 -2
  32. data/lib/simple_pvr/server/schedules_controller.rb +4 -4
  33. data/lib/simple_pvr/server/secured_controller.rb +71 -0
  34. data/lib/simple_pvr/server/shows_controller.rb +13 -8
  35. data/lib/simple_pvr/server/status_controller.rb +1 -1
  36. data/lib/simple_pvr/server/upcoming_recordings_controller.rb +1 -1
  37. data/lib/simple_pvr/version.rb +1 -1
  38. data/lib/simple_pvr/xmltv_reader.rb +37 -4
  39. data/public/css/typeahead.js-bootstrap.css +83 -0
  40. data/public/index.html +45 -37
  41. data/public/js/angular/http-auth-interceptor.js +122 -0
  42. data/public/js/app.js +4 -37
  43. data/public/js/controllers.js +22 -14
  44. data/public/js/directives.js +102 -0
  45. data/public/js/filters.js +0 -31
  46. data/public/js/services.js +64 -0
  47. data/public/js/typeahead/typeahead.min.js +7 -0
  48. data/public/partials/about.html +15 -13
  49. data/public/partials/channels.html +41 -36
  50. data/public/partials/programme.html +20 -4
  51. data/public/partials/programmeListing.html +14 -13
  52. data/public/partials/schedule.html +91 -86
  53. data/public/partials/schedules.html +28 -16
  54. data/public/partials/search.html +10 -11
  55. data/public/partials/show.html +26 -9
  56. data/public/partials/shows.html +5 -6
  57. data/public/partials/status.html +1 -1
  58. data/public/templates/loginDialog.html +30 -0
  59. data/public/templates/logoutLink.html +1 -0
  60. data/public/templates/titleSearch.html +5 -3
  61. data/simple_pvr.gemspec +6 -4
  62. data/spec/resources/dummyImage.png +1 -0
  63. data/spec/resources/programmes-with-categories.xmltv +27 -0
  64. data/spec/resources/programmes-with-credits.xmltv +24 -0
  65. data/spec/resources/programmes-with-icons.xmltv +25 -0
  66. data/spec/resources/programmes-with-presenters.xmltv +19 -0
  67. data/spec/resources/{programs-without-icon.xmltv → programmes-without-icon.xmltv} +0 -0
  68. data/spec/resources/{programs.xmltv → programmes.xmltv} +0 -4
  69. data/spec/simple_pvr/ffmpeg_spec.rb +24 -22
  70. data/spec/simple_pvr/hdhomerun_spec.rb +69 -67
  71. data/spec/simple_pvr/model/channel_spec.rb +101 -101
  72. data/spec/simple_pvr/model/programme_spec.rb +104 -104
  73. data/spec/simple_pvr/model/schedule_spec.rb +74 -74
  74. data/spec/simple_pvr/programme_icon_fetcher_spec.rb +25 -0
  75. data/spec/simple_pvr/pvr_initializer_spec.rb +40 -38
  76. data/spec/simple_pvr/recorder_spec.rb +37 -26
  77. data/spec/simple_pvr/recording_manager_spec.rb +160 -133
  78. data/spec/simple_pvr/recording_planner_spec.rb +213 -211
  79. data/spec/simple_pvr/scheduler_spec.rb +189 -172
  80. data/spec/simple_pvr/server/secured_controller_spec.rb +118 -0
  81. data/spec/simple_pvr/xmltv_reader_spec.rb +89 -41
  82. data/test/karma.conf.js +7 -4
  83. data/test/unit/filtersSpec.js +0 -36
  84. metadata +79 -63
  85. data/public/css/bootstrap-responsive.min.css +0 -9
  86. data/public/css/bootstrap.min.css +0 -9
  87. data/public/img/glyphicons-halflings-white.png +0 -0
  88. data/public/img/glyphicons-halflings.png +0 -0
  89. data/public/js/angular/angular-resource.min.js +0 -10
  90. data/public/js/angular/angular.min.js +0 -162
  91. data/public/js/bootstrap/bootstrap.min.js +0 -6
  92. data/public/js/jquery/jquery.min.js +0 -5
  93. data/test/lib/angular/angular-mocks.js +0 -1768
@@ -3,7 +3,7 @@ module SimplePvr
3
3
  def self.reload
4
4
  planner = self.new
5
5
  planner.read
6
- SimplePvr::Model::Schedule.cleanup
6
+ Model::Schedule.cleanup
7
7
  end
8
8
 
9
9
  def initialize
@@ -123,8 +123,8 @@ module SimplePvr
123
123
  end
124
124
  end
125
125
 
126
- def add_recording(title, channel, start_time, duration, programme=nil)
127
- @recordings << SimplePvr::Model::Recording.new(channel, title, start_time, duration, programme)
126
+ def add_recording(title, channel, start_time, duration, programme)
127
+ @recordings << Model::Recording.new(channel, title, start_time, duration, programme)
128
128
  end
129
129
 
130
130
  # Given a time of day as string, e.g. "10:35", returns an integer which can be used to compare with other
@@ -4,7 +4,9 @@ module SimplePvr
4
4
 
5
5
  def initialize
6
6
  @number_of_tuners = 2
7
- @upcoming_recordings, @current_recordings, @recorders = [], [nil] * @number_of_tuners, {}
7
+ @current_recordings = [nil] * @number_of_tuners
8
+ @recorders = {}
9
+ @upcoming_recordings = []
8
10
  @mutex = Mutex.new
9
11
  end
10
12
 
@@ -16,7 +18,7 @@ module SimplePvr
16
18
  end
17
19
  end
18
20
  end
19
-
21
+
20
22
  def recordings=(recordings)
21
23
  @mutex.synchronize do
22
24
  @upcoming_recordings = recordings.sort_by {|r| r.start_time }.find_all {|r| !r.expired? }
@@ -82,8 +84,14 @@ module SimplePvr
82
84
  end
83
85
 
84
86
  def stop_current_recordings_not_relevant_anymore
85
- @current_recordings.each do |recording|
86
- stop_recording(recording) if recording && !@upcoming_recordings.include?(recording)
87
+ @current_recordings.find_all {|r| r != nil }.each do |recording|
88
+ similar_recording = @upcoming_recordings.find {|r| recording.similar_to(r) }
89
+ if similar_recording
90
+ # It's (probably) the same show, so we continue recording and update with new information
91
+ similar_recording.update_with(recording)
92
+ else
93
+ stop_recording(recording)
94
+ end
87
95
  end
88
96
  end
89
97
 
@@ -8,16 +8,6 @@ module SimplePvr
8
8
  class BaseController < Sinatra::Base
9
9
  include ERB::Util
10
10
 
11
- http_username, http_password = ENV['username'], ENV['password']
12
- if http_username && http_password
13
- PvrLogger.info('Securing server with Basic HTTP Authentication')
14
- use Rack::Auth::Basic, 'Restricted Area' do |username, password|
15
- [username, password] == [http_username, http_password]
16
- end
17
- else
18
- PvrLogger.info('Beware: Unsecured server. Do not expose to the rest of the world!')
19
- end
20
-
21
11
  configure do
22
12
  set :public_folder, File.dirname(__FILE__) + '/../../../public/'
23
13
  mime_type :webm, 'video/webm'
@@ -34,23 +24,32 @@ module SimplePvr
34
24
  title: programme.title,
35
25
  subtitle: programme.subtitle,
36
26
  description: programme.description,
27
+ directors: programme.directors.map { |director| director.name },
28
+ presenters: programme.presenters.map { |presenter| presenter.name },
29
+ actors: programme.actors.map { |actor| { role_name: actor.role_name, actor_name: actor.actor_name } },
30
+ categories: programme.categories.map { |category| category.name },
37
31
  start_time: programme.start_time,
38
32
  is_scheduled: PvrInitializer.scheduler.scheduled?(programme),
39
33
  episode_num: programme.episode_num,
34
+ icon_url: programme.icon_url,
40
35
  is_outdated: programme.outdated?
41
36
  }
42
37
  end
43
38
 
44
39
  def recording_hash(show_id, recording)
45
- path = PvrInitializer.recording_manager.directory_for_show_and_episode(show_id, recording.episode)
40
+ path = PvrInitializer.recording_manager.directory_for_show_and_recording(show_id, recording.id)
46
41
  {
47
- id: recording.episode,
42
+ id: recording.id,
48
43
  show_id: show_id,
49
- episode: recording.episode,
50
44
  subtitle: recording.subtitle,
51
45
  description: recording.description,
46
+ directors: recording.directors,
47
+ presenters: recording.presenters,
48
+ actors: recording.actors,
49
+ categories: recording.categories,
52
50
  start_time: recording.start_time,
53
51
  channel_name: recording.channel,
52
+ has_icon: recording.has_icon,
54
53
  has_thumbnail: recording.has_thumbnail,
55
54
  has_webm: recording.has_webm,
56
55
  local_file_url: 'file://' + File.join(path, 'stream.ts')
@@ -1,6 +1,6 @@
1
1
  module SimplePvr
2
2
  module Server
3
- class ChannelsController < BaseController
3
+ class ChannelsController < SecuredController
4
4
  get '/' do
5
5
  Model::Channel.all_with_current_programmes.map do |channel_with_current_programmes|
6
6
  channel_with_current_programmes_hash(channel_with_current_programmes)
@@ -8,11 +8,11 @@ module SimplePvr
8
8
  end
9
9
 
10
10
  get '/:id' do |id|
11
- channel_with_current_programmes_hash(Model::Channel.with_current_programmes(id)).to_json
11
+ channel_with_current_programmes_hash(Model::Channel.with_current_programmes(id.to_i)).to_json
12
12
  end
13
13
 
14
14
  post '/:id/hide' do |id|
15
- channel = Model::Channel.get(id)
15
+ channel = Model::Channel.get(id.to_i)
16
16
  channel.hidden = true
17
17
  channel.save
18
18
  {
@@ -23,7 +23,7 @@ module SimplePvr
23
23
  end
24
24
 
25
25
  post '/:id/show' do |id|
26
- channel = Model::Channel.get(id)
26
+ channel = Model::Channel.get(id.to_i)
27
27
  channel.hidden = false
28
28
  channel.save
29
29
  {
@@ -42,7 +42,7 @@ module SimplePvr
42
42
  end
43
43
  previous_date = this_date.advance(days: -7)
44
44
  next_date = this_date.advance(days: 7)
45
- channel = Model::Channel.get(channel_id)
45
+ channel = Model::Channel.get(channel_id.to_i)
46
46
 
47
47
  days = (0..6).map do |date_advanced|
48
48
  from_date = this_date.advance(days: date_advanced)
@@ -1,6 +1,6 @@
1
1
  module SimplePvr
2
2
  module Server
3
- class ProgrammesController < BaseController
3
+ class ProgrammesController < SecuredController
4
4
  get '/title_search' do
5
5
  Model::Programme.titles_containing(params['query']).to_json
6
6
  end
@@ -10,7 +10,7 @@ module SimplePvr
10
10
  end
11
11
 
12
12
  get '/:id' do |id|
13
- programme = Model::Programme.get(id)
13
+ programme = Model::Programme.get(id.to_i)
14
14
  programme_hash(programme).to_json
15
15
  end
16
16
 
@@ -1,6 +1,6 @@
1
1
  module SimplePvr
2
2
  module Server
3
- class SchedulesController < BaseController
3
+ class SchedulesController < SecuredController
4
4
  # Must come before the "post '/:id'" below, or it won't get hit :-)
5
5
  post '/reload' do
6
6
  reload_schedules
@@ -20,13 +20,13 @@ module SimplePvr
20
20
  end
21
21
 
22
22
  get '/:id' do |id|
23
- schedule_map(Model::Schedule.get(id)).to_json
23
+ schedule_map(Model::Schedule.get(id.to_i)).to_json
24
24
  end
25
25
 
26
26
  post '/:id' do |id|
27
27
  parameters = JSON.parse(request.body.read)
28
28
  channel_id = parameters['channel'] != nil ? parameters['channel']['id'].to_i : 0
29
- schedule = Model::Schedule.get(id)
29
+ schedule = Model::Schedule.get(id.to_i)
30
30
  schedule.title = parameters['title']
31
31
  schedule.channel = channel_id > 0 ? Model::Channel.get(channel_id) : nil
32
32
 
@@ -51,7 +51,7 @@ module SimplePvr
51
51
  end
52
52
 
53
53
  delete '/:id' do |id|
54
- Model::Schedule.get(id).destroy
54
+ Model::Schedule.get(id.to_i).destroy
55
55
  reload_schedules
56
56
  ''
57
57
  end
@@ -0,0 +1,71 @@
1
+ require 'base64'
2
+
3
+ module SimplePvr
4
+ module Server
5
+ class SecuredController < BaseController
6
+ if ENV['username'] && ENV['password']
7
+ PvrLogger.info('Server secured with Basic HTTP Authentication')
8
+ else
9
+ PvrLogger.info('Beware: Unsecured server. Do not expose to the rest of the world!')
10
+ end
11
+
12
+ before do
13
+ return unless security_enabled
14
+ return if username_and_password_from_request == [http_username, http_password]
15
+
16
+ # We don't want AJAX calls to pop up the browser's own log-in dialog, so we
17
+ # give AJAX calls a special scheme. See
18
+ # http://stackoverflow.com/questions/86105/how-can-i-supress-the-browsers-authentication-dialog
19
+ # and
20
+ # http://loudvchar.blogspot.ca/2010/11/avoiding-browser-popup-for-401.html
21
+ scheme = is_ajax_call ? 'xBasic' : 'Basic'
22
+
23
+ halt 401, {
24
+ 'Content-Type' => 'text/plain',
25
+ 'Content-Length' => '0',
26
+ 'WWW-Authenticate' => "#{scheme} realm=\"SimplePVR\""
27
+ }, []
28
+ end
29
+
30
+ def http_username
31
+ ENV['username']
32
+ end
33
+
34
+ def http_password
35
+ ENV['password']
36
+ end
37
+
38
+ private
39
+ def security_enabled
40
+ http_username && http_password
41
+ end
42
+
43
+ def username_and_password_from_request
44
+ authorization = encoded_credentials_from_http_basic_authentication || encoded_credentials_from_cookie
45
+ if authorization
46
+ username_and_password = Base64.decode64(authorization)
47
+ if username_and_password =~ /(.*):(.*)/
48
+ username, password = $1, $2
49
+ return [username, password]
50
+ end
51
+ end
52
+ [nil, nil]
53
+ end
54
+
55
+ def encoded_credentials_from_http_basic_authentication
56
+ authorization = env['HTTP_AUTHORIZATION']
57
+ if authorization =~ /Basic ([a-zA-Z0-9\+\/]*[=]{0,2})/
58
+ return $1
59
+ end
60
+ end
61
+
62
+ def encoded_credentials_from_cookie
63
+ request.cookies['basicCredentials']
64
+ end
65
+
66
+ def is_ajax_call
67
+ env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,6 +1,6 @@
1
1
  module SimplePvr
2
2
  module Server
3
- class ShowsController < BaseController
3
+ class ShowsController < SecuredController
4
4
  get '/' do
5
5
  shows = PvrInitializer.recording_manager.shows
6
6
  shows.map do |show|
@@ -24,7 +24,7 @@ module SimplePvr
24
24
  end
25
25
 
26
26
  get '/:show_id/recordings/?' do |show_id|
27
- recordings = PvrInitializer.recording_manager.episodes_of(show_id)
27
+ recordings = PvrInitializer.recording_manager.recordings_of(show_id)
28
28
  recordings.map {|recording| recording_hash(show_id, recording) }.to_json
29
29
  end
30
30
 
@@ -33,28 +33,33 @@ module SimplePvr
33
33
  recording_hash(show_id, recording).to_json
34
34
  end
35
35
 
36
- delete '/:show_id/recordings/:episode' do |show_id, episode|
37
- PvrInitializer.recording_manager.delete_show_episode(show_id, episode)
36
+ delete '/:show_id/recordings/:recording_id' do |show_id, recording_id|
37
+ PvrInitializer.recording_manager.delete_show_recording(show_id, recording_id)
38
38
  ''
39
39
  end
40
40
 
41
+ get '/:show_id/recordings/:recording_id/icon' do |show_id, recording_id|
42
+ path = PvrInitializer.recording_manager.directory_for_show_and_recording(show_id, recording_id)
43
+ send_file File.join(path, 'icon')
44
+ end
45
+
41
46
  get '/:show_id/recordings/:recording_id/thumbnail.png' do |show_id, recording_id|
42
- path = PvrInitializer.recording_manager.directory_for_show_and_episode(show_id, recording_id)
47
+ path = PvrInitializer.recording_manager.directory_for_show_and_recording(show_id, recording_id)
43
48
  send_file File.join(path, 'thumbnail.png')
44
49
  end
45
50
 
46
51
  get '/:show_id/recordings/:recording_id/stream.ts' do |show_id, recording_id|
47
- path = PvrInitializer.recording_manager.directory_for_show_and_episode(show_id, recording_id)
52
+ path = PvrInitializer.recording_manager.directory_for_show_and_recording(show_id, recording_id)
48
53
  send_file File.join(path, 'stream.ts')
49
54
  end
50
55
 
51
56
  get '/:show_id/recordings/:recording_id/stream.webm' do |show_id, recording_id|
52
- path = PvrInitializer.recording_manager.directory_for_show_and_episode(show_id, recording_id)
57
+ path = PvrInitializer.recording_manager.directory_for_show_and_recording(show_id, recording_id)
53
58
  send_file File.join(path, 'stream.webm'), type: :webm
54
59
  end
55
60
 
56
61
  post '/:show_id/recordings/:recording_id/transcode' do |show_id, recording_id|
57
- path = PvrInitializer.recording_manager.directory_for_show_and_episode(show_id, recording_id)
62
+ path = PvrInitializer.recording_manager.directory_for_show_and_recording(show_id, recording_id)
58
63
  Ffmpeg.transcode_to_webm(path)
59
64
  ''
60
65
  end
@@ -1,6 +1,6 @@
1
1
  module SimplePvr
2
2
  module Server
3
- class StatusController < BaseController
3
+ class StatusController < SecuredController
4
4
  get '/' do
5
5
  {
6
6
  status_text: PvrInitializer.scheduler.status_text
@@ -1,6 +1,6 @@
1
1
  module SimplePvr
2
2
  module Server
3
- class UpcomingRecordingsController < BaseController
3
+ class UpcomingRecordingsController < SecuredController
4
4
  get '/' do
5
5
  PvrInitializer.scheduler.upcoming_recordings.map do |recording|
6
6
  {
@@ -1,3 +1,3 @@
1
1
  module SimplePvr
2
- VERSION = "1.0.0"
2
+ VERSION = '1.1.0'
3
3
  end
@@ -50,7 +50,9 @@ module SimplePvr
50
50
  end
51
51
 
52
52
  def add_programme(channel_name, programme)
53
- title_node, subtitle_node, description_node, episode_num_node = nil
53
+ title_node, subtitle_node, description_node, episode_num_node, icon_node, credits_node = nil
54
+ category_nodes = []
55
+
54
56
  programme.children.each do |child|
55
57
  case child.name
56
58
  when 'title'
@@ -58,9 +60,15 @@ module SimplePvr
58
60
  when 'sub-title'
59
61
  subtitle_node = child
60
62
  when 'desc'
61
- description_node = child
63
+ description_node = child
62
64
  when 'episode-num'
63
- episode_num_node = child
65
+ episode_num_node = child
66
+ when 'icon'
67
+ icon_node = child
68
+ when 'category'
69
+ category_nodes << child
70
+ when 'credits'
71
+ credits_node = child
64
72
  end
65
73
  end
66
74
 
@@ -70,8 +78,33 @@ module SimplePvr
70
78
  episode_num = episode_num_node ? episode_num_node.text : ''
71
79
  start_time = Time.parse(programme[:start])
72
80
  stop_time = Time.parse(programme[:stop])
81
+ icon_url = icon_node ? icon_node['src'] : nil
73
82
 
74
- Programme.add(channel_from_name(channel_name), title, subtitle, description, start_time, stop_time - start_time, episode_num)
83
+ programme = Programme.add(channel_from_name(channel_name), title, subtitle, description, start_time, stop_time - start_time, episode_num, icon_url)
84
+
85
+ if programme
86
+ add_categories(programme, category_nodes)
87
+ add_credits(programme, credits_node) if credits_node
88
+ end
89
+ end
90
+
91
+ def add_categories(programme, category_nodes)
92
+ category_nodes.each do |child|
93
+ programme.categories.create(language: child[:language], name: child.text)
94
+ end
95
+ end
96
+
97
+ def add_credits(programme, credits_node)
98
+ credits_node.children.each do |child|
99
+ case child.name
100
+ when 'director'
101
+ programme.directors.create(name: child.text)
102
+ when 'presenter'
103
+ programme.presenters.create(name: child.text)
104
+ when 'actor'
105
+ programme.actors.create(role_name: child[:role], actor_name: child.text)
106
+ end
107
+ end
75
108
  end
76
109
 
77
110
  def channel_from_name(channel_name)
@@ -0,0 +1,83 @@
1
+ /* From https://github.com/ashleydw/typeahead.js-bootstrap.css */
2
+
3
+ .twitter-typeahead {
4
+ width: 100%;
5
+ position: relative;
6
+ }
7
+
8
+ .twitter-typeahead .tt-query,
9
+ .twitter-typeahead .tt-hint {
10
+ margin-bottom: 0;
11
+ width: 100%;
12
+ height: 34px;
13
+ position: absolute;
14
+ top: 0;
15
+ left: 0;
16
+ }
17
+
18
+ .twitter-typeahead .tt-hint {
19
+ color: #a1a1a1;
20
+ z-index: 1;
21
+ padding: 6px 12px;
22
+ border: 1px solid transparent;
23
+ }
24
+
25
+ .twitter-typeahead .tt-query {
26
+ z-index: 2;
27
+ border-radius: 4px !important;
28
+ }
29
+
30
+ .input-group-addon + .twitter-typeahead > .tt-query {
31
+ border-top-left-radius: 0!important;
32
+ border-bottom-left-radius: 0!important;
33
+ }
34
+
35
+ .input-group-appended > .twitter-typeahead > .tt-query {
36
+ border-top-right-radius: 0!important;
37
+ border-bottom-right-radius: 0!important;
38
+ }
39
+
40
+ .tt-dropdown-menu {
41
+ min-width: 160px;
42
+ margin-top: 2px;
43
+ padding: 5px 0;
44
+ background-color: #fff;
45
+ border: 1px solid #ccc;
46
+ border: 1px solid rgba(0, 0, 0, .2);
47
+ *border-right-width: 2px;
48
+ *border-bottom-width: 2px;
49
+ -webkit-border-radius: 6px;
50
+ -moz-border-radius: 6px;
51
+ border-radius: 6px;
52
+ -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
53
+ -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
54
+ box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
55
+ -webkit-background-clip: padding-box;
56
+ -moz-background-clip: padding;
57
+ background-clip: padding-box;
58
+ }
59
+
60
+ .tt-suggestion {
61
+ display: block;
62
+ padding: 3px 20px;
63
+ }
64
+
65
+ .tt-suggestion.tt-is-under-cursor {
66
+ color: #fff;
67
+ background-color: #0081c2;
68
+ background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
69
+ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
70
+ background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
71
+ background-image: -o-linear-gradient(top, #0088cc, #0077b3);
72
+ background-image: linear-gradient(to bottom, #0088cc, #0077b3);
73
+ background-repeat: repeat-x;
74
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0)
75
+ }
76
+
77
+ .tt-suggestion.tt-is-under-cursor a {
78
+ color: #fff;
79
+ }
80
+
81
+ .tt-suggestion p {
82
+ margin: 0;
83
+ }