sportsflix 0.1.0.alpha.1

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