Skip to content

Commit

Permalink
Merge pull request #38 from mikz/youtube-dl
Browse files Browse the repository at this point in the history
proper youtube-dl suppport
  • Loading branch information
tomohiro committed Apr 6, 2015
2 parents 0c420f6 + ddb98da commit aa88152
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 38 deletions.
11 changes: 10 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ rvm:
- 2.2.0
- 2.2.1

env:
YOUTUBE_DL_VERSION=2015.04.03

cache: bundler

before_install:
- sudo apt-get -qq update
- sudo apt-get -qq install youtube-dl rdnssd libavahi-compat-libdnssd-dev
- sudo apt-get -qq install rdnssd libavahi-compat-libdnssd-dev
- sudo curl https://yt-dl.org/downloads/${YOUTUBE_DL_VERSION}/youtube-dl -o /usr/local/bin/youtube-dl
- sudo chmod a+x /usr/local/bin/youtube-dl

before_script:
- which youtube-dl
- youtube-dl --version

script: bundle exec rake spec
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ File extension | MIME type | Ruby `mime-types`
.mp4 | video/mpeg4 | application/mp4, video/mp4


# OSX Service

You can create Automator Service, that opens URL from your browser in airplayer.
![automator service](https://cloud.githubusercontent.com/assets/154571/6997755/aa2599e8-dbc8-11e4-8cc4-9671d9cd8ad7.png)


LICENSE
--------------------------------------------------------------------------------

Expand Down
4 changes: 3 additions & 1 deletion lib/airplayer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

module AirPlayer
require 'airplayer/version'
require 'airplayer/app'
require 'airplayer/controller'
require 'airplayer/device'
require 'airplayer/playlist'
require 'airplayer/media'
require 'airplayer/youtube_dl'

require 'airplayer/app'
end
5 changes: 5 additions & 0 deletions lib/airplayer/app.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@

module AirPlayer
class App < Thor
class_option :youtube_dl, desc: 'path to youtube-dl', default: YoutubeDl.default_path

desc 'play <URI|PATH> [-r|--repeat] [-s|--shuffle] [-d=|--device=]', 'Play video(URI[Podcast URI, YouTube] or Path[local video file, directory])'
method_option :repeat, aliases: '-r', desc: 'Repeat play', type: :boolean
method_option :shuffle, aliases: '-s', desc: 'Shuffle play', type: :boolean
method_option :device, aliases: '-d', desc: 'Device number', type: :numeric
def play(target)
YoutubeDl.path = options[:youtube_dl]
controller = Controller.new(device: options.fetch('device', 0))

Playlist.new(options).add(target).entries do |media|
controller.play(media)
controller.pause
end
rescue Interrupt # capture Ctrl-C
end

desc 'devices', 'Show AirPlay devices'
Expand Down
2 changes: 1 addition & 1 deletion lib/airplayer/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ def play(media)
puts " Device: #{@device.name} (Resolution: #{@device.info.resolution})"

@progressbar = ProgressBar.create(format: ' %a |%b%i| %p%% %t')
@player = @device.play(media.path)
@player.progress -> playback {
@progressbar.title = 'Streaming'
@progressbar.progress = playback.percent if playback.percent
}
@player = @device.play(media.path)
@player.wait
end

Expand Down
56 changes: 22 additions & 34 deletions lib/airplayer/media.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ module AirPlayer
video/mpeg4
)

SUPPORTED_DOMAINS = %w(
youtube
youtu.be
)

class Media
attr_reader :title, :path, :type

Expand All @@ -35,51 +30,44 @@ def initialize(target)
@title = File.basename(path)
@type = :file
else
uri = URI.encode(target)
@path = online_media_path(uri)
@title = online_media_title(uri)
@path = YoutubeDl.get_url(target)
@title = YoutubeDl.get_title(target)
@type = :url
end
end

def self.playable?(path)
MIME::Types.of(path).map(&:simplified).each do |mimetype|
return SUPPORTED_MIME_TYPES.include?(mimetype)
class << self
def playable?(path)
if is_url?(path)
YoutubeDl.supports?(path) || supported_mime_type?(YoutubeDl.filename(path))
else
supported_mime_type?(path)
end
end

host = URI.parse(URI.escape(path)).host
SUPPORTED_DOMAINS.each do |domain|
return true if host =~ /#{domain}/
def is_url?(path)
uri = URI(path)
uri.scheme && uri.absolute?
rescue URI::InvalidURIError
false
end

false
def supported_mime_type?(path)
MIME::Types.of(path).map(&:simplified).each do |mimetype|
return SUPPORTED_MIME_TYPES.include?(mimetype)
end

false
end
end


def file?
@type == :file
end

def url?
@type == :url
end

private
def online_media_path(uri)
case URI.parse(uri).host
when /youtube|youtu\.be/
uri = `youtube-dl -g #{uri}`
else
uri
end
end

def online_media_title(uri)
case URI.parse(uri).host
when /youtube|youtu\.be/
title = `youtube-dl -e #{uri}`
else
title = File.basename(uri)
end
end
end
end
3 changes: 2 additions & 1 deletion lib/airplayer/playlist.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def add(item)
when :url
push(Media.new(item))
end

self
end

Expand All @@ -33,7 +34,7 @@ def entries(&blk)
def type(item)
if Dir.exists?(File.expand_path(item))
:local_dir
elsif Media.playable? item
elsif Media.playable?(item)
:url
elsif item.match(/.+(xml|rss)$/)
:podcast
Expand Down
100 changes: 100 additions & 0 deletions lib/airplayer/youtube_dl.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
require 'shellwords'

module AirPlayer
class YoutubeDl

attr_reader :path

def initialize(path = self.class.path || self.class.default_path)
@path = path

if @path
File.exist?(@path) or raise "youtube-dl could not be found at #{@path}"
end

@output = '2> /dev/null'.freeze
end

def enabled?
@path && ! @path.empty?
end

EXTRACT_EXTRACTOR_AND_URL = %r{
(?<extractor>.+)\n # extractor name and new line
\s+(:?.+) # the url if the extractor supports it
}x

GENERIC_EXTRACTOR = 'generic'.freeze

def supports?(url)
extractors = list_extractors(url).scan(EXTRACT_EXTRACTOR_AND_URL).flatten
extractors.delete(GENERIC_EXTRACTOR)
! extractors.empty?
end

def list_extractors(*urls)
execute('--list-extractors', *urls)
end

def get_url(url)
execute('--get-url', url)
end

def get_filename(url)
execute('--get-filename', url)
end

def get_title(url)
execute('--get-title', url)
end

def execute(*args)
return '' unless enabled?
escape = Shellwords.method(:escape)
parts = [ path, args.flat_map(&escape), @output ]
%x`#{parts.flatten.join(' ')}`.strip
end

class << self
attr_accessor :path

def enabled?
path && ! path.empty?
end

def default_path
@default_path ||= `which youtube-dl 2> /dev/null`.strip
end

def get_url(uri)
if enabled?
new.get_url(uri)
else
uri
end
end

def supports?(path)
if enabled?
new.supports?(path)
end
end

def filename(url)
if enabled?
new.get_filename(url)
else
File.basename(url)
end
end

def get_title(uri)
if enabled?
new.get_title(uri)
else
File.basename(uri)
end
end
end
end
end
15 changes: 15 additions & 0 deletions spec/airplayer/media_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ module AirPlayer
expect(media.playable?('NOT_PLAYABLE_FILE')).to be false
end
end

context 'with urls' do
it 'delegates to YoutubeDl' do

expect(YoutubeDl).to receive(:supports?)
.with('http://example.com/video.mp4').and_return(true)
expect(media.playable?('http://example.com/video.mp4')).to be

expect(YoutubeDl).to receive(:supports?)
.with('http://example.com/not-video').and_return(false)
expect(media.playable?('http://example.com/not-video')).not_to be
end
end
end

describe '.file?' do
Expand All @@ -42,6 +55,8 @@ module AirPlayer

describe '.url?' do
context 'with given URL' do
before { allow(YoutubeDl).to receive(:enabled?).and_return(false) }

it 'returns true' do
expect(media.new('http://example.com/video.mp4').url?).to be true
end
Expand Down
26 changes: 26 additions & 0 deletions spec/airplayer/playlist_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@

module AirPlayer
describe Playlist do
include FakeFS::SpecHelpers

let (:playlist) do
AirPlayer::Playlist.new
end

# works even without this, but it prevents connecting to internet
before{ allow(YoutubeDl).to receive(:enabled?).and_return(false) }

describe '.add' do
context 'with local directory' do
it 'returns media type is local file' do
Expand All @@ -31,12 +36,19 @@ module AirPlayer

context 'with multiple files' do
it 'have multiple files' do
FakeFS do
FileUtils.touch('video.m4v')
FileUtils.touch('video.mp4')
end

expect(playlist.add('video.mp4').size).to eq 1
expect(playlist.add('video.m4v').size).to eq 2
end
end

context 'with podcast RSS' do
before { FileUtils.mkpath(Dir.tmpdir) }

it 'returns media instances' do
playlist.add('http://rss.cnn.com/services/podcasting/cnnnewsroom/rss.xml')
playlist.entries do |media|
Expand All @@ -47,12 +59,26 @@ module AirPlayer

context 'with local file' do
it 'returns media instances' do
FakeFS do
FileUtils.touch('video.mp4')
end

playlist.add('video.mp4')
playlist.entries do |media|
expect(media).to be_kind_of AirPlayer::Media
end
end
end

context 'with stream url' do
it 'returns media instances' do
playlist.add('https://www.stream.cz/vesele-velikonoce/10005380-nadiwich')
playlist.entries do |media|
expect(media).to be_kind_of AirPlayer::Media
expect(media.type).to eq(:url)
end
end
end
end
end
end
Loading

0 comments on commit aa88152

Please sign in to comment.