sportsflix 0.1.0.alpha.1

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,57 @@
1
+ # sportsflix
2
+
3
+ Watch the best sports stream in HD from the command line
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ gem install sportsflix
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Examples
14
+
15
+ **Watch Benfica game**
16
+ ```
17
+ sflix watch --video-format=mkv --video-player=vlc --club=BENFICA
18
+ ```
19
+
20
+ **Watch game on Mac OS with VLC**
21
+ ```
22
+ sflix watch --video-player-path="/Applications/VLC.app/Contents/MacOS/VLC"
23
+ ```
24
+
25
+ ### Help
26
+
27
+ Commands:
28
+ sflix help [COMMAND] # Describe available commands or one specific command
29
+ sflix watch # watch stream in the chosen player
30
+
31
+ Options:
32
+ vvv, [--verbose], [--no-verbose]
33
+ o, [--offset=N]
34
+ # Default: 0
35
+ f, [--video-format=VIDEO-FORMAT]
36
+ # Default: mp4
37
+ c, [--club=CLUB]
38
+ p, [--video-player=VIDEO-PLAYER]
39
+ # Default: vlc
40
+ pp, [--video-player-path=VIDEO-PLAYER-PATH]
41
+ # Default: vlc
42
+ ni, [--no-interactive]
43
+ s, [--server-only], [--no-server-only]
44
+
45
+ ## Development
46
+
47
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
48
+
49
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
50
+
51
+ ## Contributing
52
+
53
+ Bug reports and pull requests are welcome on GitHub at https://github.com/rtfpessoa/sportsflix. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
54
+
55
+ ## Copyright
56
+
57
+ Copyright (c) 2017-present Rodrigo Fernandes. See [LICENSE](https://github.com/rtfpessoa/sportsflix/blob/master/LICENSE.md) for details.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'sportsflix'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "sportsflix/cli"
4
+
5
+ Sportsflix::CLI.start(ARGV)
@@ -0,0 +1,11 @@
1
+ machine:
2
+ ruby:
3
+ version: 2.3.1
4
+ dependencies:
5
+ override:
6
+ - gem install bundler -v 1.14.3
7
+ - bundle check || bundle install
8
+ test:
9
+ override:
10
+ - bundle exec rubocop
11
+ - bundle exec rake
@@ -0,0 +1,124 @@
1
+ require 'net/http'
2
+ require 'socket'
3
+ require 'sportsflix/players/vlc'
4
+ require 'sportsflix/providers/arenavision'
5
+ require 'sportsflix/utils/exec'
6
+
7
+ module Sportsflix
8
+ class API
9
+
10
+ def initialize(options)
11
+ @verbose = options[:verbose]
12
+ @stream_offset = options[:offset]
13
+ @club_name = options[:club]
14
+ @no_interactive = options['no-interactive']
15
+ @video_player = options['video-player'].to_sym
16
+
17
+ @arenavision_client = Providers::Arenavision::Client.new(options)
18
+ @executor = Sportsflix::Utils::Executor.new(options)
19
+ @players = {
20
+ :vlc => Sportsflix::Players::VLC::Client.new(options)
21
+ }
22
+ end
23
+
24
+ def watch
25
+ streams = @arenavision_client.list_streams
26
+
27
+ if @club_name
28
+ streams = streams.select do |stream|
29
+ stream[:game].downcase.include?(@club_name.downcase)
30
+ end
31
+ end
32
+
33
+ response = ask_choose_stream(streams)
34
+
35
+ stream_uri = @arenavision_client.get_stream_uri(response[:channel_number])
36
+
37
+ unless @players.key?(@video_player)
38
+ puts "Unable to find client for #{@video_player} player"
39
+ exit(1)
40
+ end
41
+
42
+ player = @players[@video_player]
43
+ player.start({
44
+ :proxy => response[:stream][:proxy],
45
+ :uri => stream_uri
46
+ })
47
+ end
48
+
49
+ private
50
+ def ask_choose_stream(streams)
51
+ selection = ask_stream(streams)
52
+ selected_stream = streams[selection]
53
+
54
+ stream_languages = selected_stream[:stream_nr]
55
+ language_selection = ask_language(stream_languages)
56
+
57
+ stream_channels = stream_languages[language_selection]
58
+ stream_channel_nr = ask_channel(stream_channels)
59
+
60
+ {
61
+ :stream => selected_stream,
62
+ :channel_number => stream_channel_nr
63
+ }
64
+ end
65
+
66
+ def ask_stream(streams)
67
+ selection = 0
68
+ if streams.empty?
69
+ puts "There are no streams matching your query #{@club_name}"
70
+ exit(1)
71
+ elsif streams.length > 1 && !@no_interactive
72
+ streams.each_with_index do |stream, idx|
73
+ puts "#{idx}) #{stream[:game]}"
74
+ end
75
+
76
+ printf 'Choose the game: '
77
+ selection = STDIN.gets.chomp.to_i
78
+ puts ''
79
+ end
80
+ selection
81
+ end
82
+
83
+ def ask_language(stream_channels)
84
+ channel_selection = 0
85
+ if stream_channels.length > 1 && !@no_interactive
86
+ stream_channels.each_with_index do |channel, idx|
87
+ puts "#{idx}) #{channel[:language]}"
88
+ end
89
+
90
+ printf 'Choose the language: '
91
+ channel_selection = STDIN.gets.chomp.to_i
92
+ puts ''
93
+ end
94
+ channel_selection
95
+ end
96
+
97
+ def ask_channel(stream_channels)
98
+ stream_channel_nr = stream_channels[:start]
99
+ if stream_channels[:start] != stream_channels[:end] && !@no_interactive
100
+ possible_channels = (stream_channels[:start]..stream_channels[:end]).to_a
101
+ possible_channels.each_with_index do |nr, idx|
102
+ if @stream_offset == idx
103
+ puts "*#{idx}) Channel #{nr}"
104
+ else
105
+ puts " #{idx}) Channel #{nr}"
106
+ end
107
+ end
108
+
109
+ printf 'Choose the channel number: '
110
+ stream_channel_idx_raw = STDIN.gets.chomp
111
+ puts ''
112
+
113
+ begin
114
+ stream_channel_offset = Integer(stream_channel_idx_raw)
115
+ rescue ArgumentError
116
+ stream_channel_offset = @stream_offset
117
+ end
118
+ stream_channel_nr = possible_channels[stream_channel_offset]
119
+ end
120
+ stream_channel_nr
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,30 @@
1
+ require 'thor'
2
+ require 'sportsflix'
3
+
4
+ module Sportsflix
5
+
6
+ class CLI < Thor
7
+
8
+ # SLB, SLB, SLB, SLB, Glorioso SLB, Glorioso SLB !!!
9
+ DEFAULT_TEAM = 'BENFICA'
10
+ DEFAULT_OFFSET = 0
11
+ DEFAULT_VIDEO_FORMAT = 'mp4'
12
+ DEFAULT_VIDEO_PLAYER = 'vlc'
13
+ DEFAULT_VIDEO_PLAYER_PATH = DEFAULT_VIDEO_PLAYER
14
+
15
+ class_option('verbose', { :aliases => :vvv, :type => :boolean, :default => false })
16
+ class_option('offset', { :aliases => :o, :type => :numeric, :default => DEFAULT_OFFSET })
17
+ class_option('video-format', { :aliases => :f, :type => :string, :default => DEFAULT_VIDEO_FORMAT })
18
+ class_option('club', { :aliases => :c, :type => :string })
19
+ class_option('video-player', { :aliases => :p, :type => :string, :default => DEFAULT_VIDEO_PLAYER })
20
+ class_option('video-player-path', { :aliases => :pp, :type => :string, :default => DEFAULT_VIDEO_PLAYER_PATH })
21
+ class_option('no-interactive', { :aliases => :ni, :type => :boolean, :default => false })
22
+ class_option('server-only', { :aliases => :s, :type => :boolean, :default => false })
23
+
24
+ desc('watch', 'watch stream in the chosen player')
25
+ def watch
26
+ api = API.new(options)
27
+ api.watch
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,59 @@
1
+ require 'sportsflix/utils/exec'
2
+
3
+ module Sportsflix
4
+ module Players
5
+ module Proxies
6
+ module Acestream
7
+ class Client
8
+
9
+ ACESTREAM_STREAM_URI_PREFIX = 'acestream://'
10
+ ACESTREAM_PROXY_IMAGE_NAME = ['ikatson/aceproxy', 'zveronline/aceproxy']
11
+ ACESTREAM_PROXY_IMAGE_NAME_IDX = 0
12
+ ACESTREAM_PROXY_DOCKER_NAME = 'acestream'
13
+
14
+ def initialize(options)
15
+ @verbose = options[:verbose]
16
+ @video_format = options['video-format']
17
+
18
+ @executor = Sportsflix::Utils::Executor.new(options)
19
+ end
20
+
21
+ def start
22
+ @executor.run %{docker pull #{ACESTREAM_PROXY_IMAGE_NAME[ACESTREAM_PROXY_IMAGE_NAME_IDX]}}
23
+ @executor.run %{docker run -d -t -p 8000:8000 --name #{ACESTREAM_PROXY_DOCKER_NAME} #{ACESTREAM_PROXY_IMAGE_NAME[ACESTREAM_PROXY_IMAGE_NAME_IDX]}}
24
+ end
25
+
26
+ def stop
27
+ @executor.run %{docker rm -f #{ACESTREAM_PROXY_DOCKER_NAME}}
28
+ end
29
+
30
+ def url(uri)
31
+ stream_uuid = uri.sub(ACESTREAM_STREAM_URI_PREFIX, '')
32
+ machine_ip = local_ip
33
+ "http://#{machine_ip}:8000/pid/#{stream_uuid}/stream.#{@video_format}"
34
+ end
35
+
36
+ private
37
+ def local_ip
38
+ # Turn off reverse DNS resolution temporarily
39
+ orig = Socket.do_not_reverse_lookup
40
+ Socket.do_not_reverse_lookup = true
41
+
42
+ begin
43
+ UDPSocket.open do |s|
44
+ # Google
45
+ s.connect '64.233.187.99', 1
46
+ s.addr.last
47
+ end
48
+ rescue
49
+ return '127.0.0.1'
50
+ end
51
+ ensure
52
+ # Restore DNS resolution
53
+ Socket.do_not_reverse_lookup = orig
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ module Sportsflix
2
+ module Players
3
+ module Proxies
4
+ module Default
5
+ class Client
6
+
7
+ def initialize(options)
8
+ @verbose = options[:verbose]
9
+ end
10
+
11
+ def start
12
+ puts 'Starting default proxy' if @verbose
13
+ end
14
+
15
+ def stop
16
+ puts 'Stopping default proxy' if @verbose
17
+ end
18
+
19
+ def url(uri)
20
+ uri
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,84 @@
1
+ require 'sportsflix/players/proxies/acestream'
2
+ require 'sportsflix/players/proxies/default'
3
+ require 'sportsflix/utils/exec'
4
+
5
+ module Sportsflix
6
+ module Players
7
+ module VLC
8
+ class Client
9
+
10
+ ALTERNATIVE_PLAYER_PATHS = [
11
+ 'vlc',
12
+ '/Applications/VLC.app/Contents/MacOS/VLC',
13
+ '/c/Program Files/VideoLAN/VLC/vlc.exe',
14
+ '/mnt/c/Program Files/VideoLAN/VLC/vlc.exe'
15
+ ]
16
+
17
+ def initialize(options)
18
+ @verbose = options[:verbose]
19
+ @server_only = options['server-only']
20
+ @video_player_path = options['video-player-path']
21
+
22
+ @executor = Sportsflix::Utils::Executor.new(options)
23
+ @stream_proxies = {
24
+ :default => Sportsflix::Players::Proxies::Default::Client.new(options),
25
+ :acestream => Sportsflix::Players::Proxies::Acestream::Client.new(options)
26
+ }
27
+ end
28
+
29
+ def start(stream)
30
+ proxy = @stream_proxies[stream[:proxy]]
31
+
32
+ proxy.stop
33
+ proxy.start
34
+
35
+ # Waiting for proxy to start
36
+ sleep(10)
37
+
38
+ unless @server_only
39
+ video_player = find_video_player
40
+ stream_final_url = proxy.url(stream[:uri])
41
+
42
+ puts "Playing #{stream_final_url}"
43
+ @executor.run %{#{video_player} #{stream_final_url}}
44
+
45
+ proxy.stop
46
+ end
47
+ end
48
+
49
+ private
50
+ def find_video_player
51
+ unless which(@video_player_path) || File.exist?(@video_player_path)
52
+ puts "Could not find video player #{@video_player_path}, searching for alternatives ..."
53
+
54
+ ALTERNATIVE_PLAYER_PATHS.each do |path|
55
+ if which(path) || File.exist?(path)
56
+ puts "Found VLC player in #{path}"
57
+ return path
58
+ end
59
+ end
60
+
61
+ puts 'Could not find vlc in any of the alternative locations'
62
+ exit(1)
63
+ end
64
+
65
+ @video_player_path
66
+ end
67
+
68
+ # Cross-platform way of finding an executable in the $PATH.
69
+ # Source: http://stackoverflow.com/posts/5471032/revisions
70
+ def which(cmd)
71
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
72
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
73
+ exts.each { |ext|
74
+ bin = File.join(path, "#{cmd}#{ext}")
75
+ return File.executable?(bin) && !File.directory?(bin)
76
+ }
77
+ end
78
+
79
+ false
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,88 @@
1
+ require 'net/http'
2
+ require 'oga'
3
+
4
+ module Sportsflix
5
+ module Providers
6
+ module Arenavision
7
+ class Client
8
+ ARENAVISION_BASE_URL = ['http://arenavision.in', 'http://arenavision.ru']
9
+ ARENAVISION_DEFAULT_URL_IDX = 0
10
+
11
+ def initialize(options)
12
+ @verbose = options[:verbose]
13
+ @club_name = options[:club]
14
+ @server_only = options['server-only']
15
+
16
+ @schedule = get_page_contents("#{ARENAVISION_BASE_URL[ARENAVISION_DEFAULT_URL_IDX]}/schedule")
17
+ end
18
+
19
+ def list_streams
20
+ streams = @schedule.css('table tr')
21
+ # Remove first element
22
+ streams = streams.drop(1)
23
+ # Remove last element
24
+ streams.pop(2)
25
+
26
+ # Remove weird empty lines with non-breaking spaces ?!?
27
+ streams = streams.select do |item|
28
+ item_text = item.css('td:nth-child(1)').text
29
+ item_text = item_text.force_encoding('UTF-8')
30
+ item_text =item_text.delete(' ').strip
31
+ not item_text.empty?
32
+ end
33
+
34
+ streams.map do |item|
35
+ {
36
+ :date => clean_str(item.css('td:nth-child(1)').text),
37
+ :hour => clean_str(item.css('td:nth-child(2)').text),
38
+ :sport => clean_str(item.css('td:nth-child(3)').text),
39
+ :competition => clean_str(item.css('td:nth-child(4)').text),
40
+ :game => clean_str(item.css('td:nth-child(5)').text),
41
+ :stream_nr => parse_stream_ids(clean_str(item.css('td:nth-child(6)').text)),
42
+ :proxy => :acestream
43
+ }
44
+ end
45
+ end
46
+
47
+ def get_stream_uri(stream_nr)
48
+ stream_raw = get_page_contents("#{ARENAVISION_BASE_URL[ARENAVISION_DEFAULT_URL_IDX]}/av#{stream_nr}")
49
+ stream_raw.css('p[class="auto-style1"] a').first.get('href')
50
+ end
51
+
52
+ private
53
+ def get_page_contents(raw_url)
54
+ url = URI.parse(raw_url)
55
+ enum = Enumerator.new do |yielder|
56
+ Net::HTTP.start(url.host, url.port) do |http|
57
+ http.request_get(url.path) do |response|
58
+ response.read_body do |chunk|
59
+ yielder << chunk
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ Oga.parse_xml(enum)
66
+ end
67
+
68
+ def parse_stream_ids(raw_stream)
69
+ matches = raw_stream.scan(/(([0-9]+)(?:-([0-9]+))? \[(.+?)\])/)
70
+ matches.map do |match|
71
+ {
72
+ :start => match[1].to_i,
73
+ :end => (match[2] || match[1]).to_i,
74
+ :language => match[3]
75
+ }
76
+ end
77
+ end
78
+
79
+ def clean_str(str)
80
+ str.force_encoding('UTF-8')
81
+ .gsub("\n\t\t", ' ')
82
+ .strip
83
+ end
84
+
85
+ end
86
+ end
87
+ end
88
+ end