2018-04-24 21:28:42 +02:00
import os
2017-12-27 10:18:51 +01:00
import re
import subprocess
2018-04-19 00:30:39 +02:00
import urllib . request
import urllib . parse
2018-10-22 18:26:11 +02:00
from platypush . context import get_backend , get_plugin
2018-04-20 12:17:27 +02:00
from platypush . plugins . media import PlayerState
2018-04-19 22:42:28 +02:00
from platypush . message . event . video import VideoPlayEvent , VideoPauseEvent , \
2018-04-20 09:27:57 +02:00
VideoStopEvent , NewPlayingVideoEvent
2017-12-27 10:18:51 +01:00
2018-07-06 02:08:38 +02:00
from platypush . plugins import Plugin , action
2017-12-27 10:18:51 +01:00
class VideoOmxplayerPlugin ( Plugin ) :
2018-06-25 19:57:43 +02:00
"""
Plugin to control video and media playback on your Raspberry Pi or
ARM - compatible device using OMXPlayer .
It can play local files , remote URLs , YouTube URLs and it supports torrents
search , download and play .
Requires :
* * * omxplayer * * installed on your system ( see your distro instructions )
* * * omxplayer - wrapper * * ( ` ` pip install omxplayer - wrapper ` ` )
* * * python - libtorrent * * ( ` ` pip install python - libtorrent ` ` ) , optional for Torrent support
* * * youtube - dl * * installed on your system ( see your distro instructions ) , optional for YouTube support
"""
2018-07-16 23:17:00 +02:00
# Supported video extensions
2018-04-24 21:28:42 +02:00
video_extensions = {
' .avi ' , ' .flv ' , ' .wmv ' , ' .mov ' , ' .mp4 ' , ' .m4v ' , ' .mpg ' , ' .mpeg ' ,
2018-06-02 21:56:46 +02:00
' .rm ' , ' .swf ' , ' .vob ' , ' .mkv '
2018-04-24 21:28:42 +02:00
}
2018-10-22 18:26:11 +02:00
def __init__ ( self , args = [ ] , media_dirs = [ ] , download_dir = None , * argv , * * kwargs ) :
2018-06-25 19:57:43 +02:00
"""
: param args : Arguments that will be passed to the OMXPlayer constructor ( e . g . subtitles , volume , start position , window size etc . ) see https : / / github . com / popcornmix / omxplayer #synopsis and http://python-omxplayer-wrapper.readthedocs.io/en/latest/omxplayer/#omxplayer.player.OMXPlayer
: type args : list
: param media_dirs : Directories that will be scanned for media files when a search is performed ( default : none )
: type media_dirs : list
: param download_dir : Directory where the videos / torrents will be downloaded ( default : none )
: type download_dir : str
"""
2018-04-25 00:13:17 +02:00
super ( ) . __init__ ( * argv , * * kwargs )
2018-04-24 21:28:42 +02:00
2017-12-27 10:18:51 +01:00
self . args = args
2018-04-25 11:29:03 +02:00
self . media_dirs = set (
2018-04-24 21:28:42 +02:00
filter (
lambda _ : os . path . isdir ( _ ) ,
map (
lambda _ : os . path . abspath ( os . path . expanduser ( _ ) ) ,
media_dirs
)
)
)
2018-04-25 12:03:15 +02:00
if download_dir :
self . download_dir = os . path . abspath ( os . path . expanduser ( download_dir ) )
2018-04-25 11:29:03 +02:00
if not os . path . isdir ( self . download_dir ) :
raise RuntimeError ( ' download_dir [ {} ] is not a valid directory '
. format ( self . download_dir ) )
self . media_dirs . add ( self . download_dir )
2017-12-27 10:18:51 +01:00
self . player = None
2018-04-19 00:32:20 +02:00
self . videos_queue = [ ]
2017-12-27 10:18:51 +01:00
2018-07-06 02:08:38 +02:00
@action
2017-12-27 10:18:51 +01:00
def play ( self , resource ) :
2018-06-25 19:57:43 +02:00
"""
Play a resource .
: param resource : Resource to play . Supported types :
* Local files ( format : ` ` file : / / < path > / < file > ` ` )
* Remote videos ( format : ` ` https : / / < url > / < resource > ` ` )
* YouTube videos ( format : ` ` https : / / www . youtube . com / watch ? v = < id > ` ` )
2018-10-22 18:26:11 +02:00
* Torrents ( format : Magnet links , Torrent URLs or local Torrent files )
2018-06-25 19:57:43 +02:00
"""
2018-12-12 22:31:36 +01:00
from dbus . exceptions import DBusException
2017-12-27 10:18:51 +01:00
if resource . startswith ( ' youtube: ' ) \
or resource . startswith ( ' https://www.youtube.com/watch?v= ' ) :
resource = self . _get_youtube_content ( resource )
2018-04-25 11:29:03 +02:00
elif resource . startswith ( ' magnet:? ' ) :
2018-10-22 18:26:11 +02:00
torrents = get_plugin ( ' torrent ' )
response = torrents . download ( resource , download_dir = self . download_dir )
resources = [ f for f in response . output if self . _is_video_file ( f ) ]
2018-04-25 11:29:03 +02:00
if resources :
2018-10-22 18:51:00 +02:00
self . videos_queue = sorted ( resources )
2018-04-25 11:29:03 +02:00
resource = self . videos_queue . pop ( 0 )
else :
2018-07-06 02:08:38 +02:00
raise RuntimeError ( ' Unable to download torrent {} ' . format ( resource ) )
2017-12-27 10:18:51 +01:00
2018-06-06 20:09:18 +02:00
self . logger . info ( ' Playing {} ' . format ( resource ) )
2018-04-19 10:18:46 +02:00
2018-04-24 14:36:05 +02:00
if self . player :
try :
self . player . stop ( )
self . player = None
except Exception as e :
2018-06-06 20:09:18 +02:00
self . logger . exception ( e )
self . logger . warning ( ' Unable to stop a previously running instance ' +
2018-04-24 14:36:05 +02:00
' of OMXPlayer, trying to play anyway ' )
2017-12-27 11:27:06 +01:00
try :
2018-12-12 22:31:36 +01:00
from omxplayer import OMXPlayer
2017-12-27 11:27:06 +01:00
self . player = OMXPlayer ( resource , args = self . args )
2018-04-25 00:13:17 +02:00
self . _init_player_handlers ( )
2017-12-27 11:27:06 +01:00
except DBusException as e :
2018-06-06 20:09:18 +02:00
self . logger . warning ( ' DBus connection failed: you will probably not ' +
2017-12-27 11:27:06 +01:00
' be able to control the media ' )
2018-06-06 20:09:18 +02:00
self . logger . exception ( e )
2017-12-27 11:27:06 +01:00
2017-12-27 10:18:51 +01:00
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2017-12-27 10:18:51 +01:00
def pause ( self ) :
2018-06-25 19:57:43 +02:00
""" Pause the playback """
2017-12-27 10:18:51 +01:00
if self . player : self . player . play_pause ( )
2018-07-06 02:08:38 +02:00
@action
2017-12-27 10:18:51 +01:00
def stop ( self ) :
2018-06-25 19:57:43 +02:00
""" Stop the playback """
2017-12-27 10:18:51 +01:00
if self . player :
self . player . stop ( )
self . player . quit ( )
self . player = None
2018-04-19 22:42:28 +02:00
2018-09-25 00:26:06 +02:00
return { ' status ' : ' stop ' }
2018-07-06 02:08:38 +02:00
@action
2017-12-27 10:18:51 +01:00
def voldown ( self ) :
2018-06-25 19:57:43 +02:00
""" Volume down by 10 % """
2017-12-27 10:18:51 +01:00
if self . player :
2017-12-27 12:02:47 +01:00
self . player . set_volume ( max ( - 6000 , self . player . volume ( ) - 1000 ) )
2017-12-27 10:18:51 +01:00
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2017-12-27 10:18:51 +01:00
def volup ( self ) :
2018-06-25 19:57:43 +02:00
""" Volume up by 10 % """
2017-12-27 10:18:51 +01:00
if self . player :
2017-12-27 11:01:07 +01:00
self . player . set_volume ( min ( 0 , self . player . volume ( ) + 1000 ) )
2017-12-27 10:18:51 +01:00
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2017-12-27 10:18:51 +01:00
def back ( self ) :
2018-06-25 19:57:43 +02:00
""" Back by 30 seconds """
2017-12-27 10:18:51 +01:00
if self . player :
self . player . seek ( - 30 )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2017-12-27 10:18:51 +01:00
def forward ( self ) :
2018-06-25 19:57:43 +02:00
""" Forward by 30 seconds """
2017-12-27 10:18:51 +01:00
if self . player :
self . player . seek ( + 30 )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-19 00:30:39 +02:00
def next ( self ) :
2018-06-25 19:57:43 +02:00
""" Play the next track/video """
2018-04-19 00:30:39 +02:00
if self . player :
self . player . stop ( )
if self . videos_queue :
2018-04-19 00:39:59 +02:00
video = self . videos_queue . pop ( 0 )
return self . play ( video )
2018-04-19 00:30:39 +02:00
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def hide_subtitles ( self ) :
2018-06-25 19:57:43 +02:00
""" Hide the subtitles """
2018-04-20 12:17:27 +02:00
if self . player : self . player . hide_subtitles ( )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def hide_video ( self ) :
2018-06-25 19:57:43 +02:00
""" Hide the video """
2018-04-20 12:17:27 +02:00
if self . player : self . player . hide_video ( )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def is_playing ( self ) :
2018-06-25 19:57:43 +02:00
"""
: returns : True if it ' s playing, False otherwise
"""
2018-04-20 12:17:27 +02:00
if self . player : return self . player . is_playing ( )
else : return False
2018-07-06 02:08:38 +02:00
@action
2018-06-25 19:57:43 +02:00
def load ( self , resource , pause = False ) :
"""
Load a resource / video in the player .
: param pause : If set , load the video in paused mode ( default : False )
: type pause : bool
"""
if self . player : self . player . load ( resource , pause )
2018-04-20 12:17:27 +02:00
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def metadata ( self ) :
2018-06-25 19:57:43 +02:00
""" Get the metadata of the current video """
2018-07-06 02:08:38 +02:00
if self . player :
return self . player . metadata ( )
2018-04-20 12:17:27 +02:00
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def mute ( self ) :
2018-06-25 19:57:43 +02:00
""" Mute the player """
2018-04-20 12:17:27 +02:00
if self . player : self . player . mute ( )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def unmute ( self ) :
2018-06-25 19:57:43 +02:00
""" Unmute the player """
2018-04-20 12:17:27 +02:00
if self . player : self . player . unmute ( )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def seek ( self , relative_position ) :
2018-06-25 19:57:43 +02:00
"""
Seek backward / forward by the specified number of seconds
: param relative_position : Number of seconds relative to the current cursor
: type relative_position : int
"""
2018-04-20 12:17:27 +02:00
if self . player : self . player . seek ( relative_position )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def set_position ( self , position ) :
2018-06-25 19:57:43 +02:00
"""
Seek backward / forward to the specified absolute position
: param position : Number of seconds from the start
: type position : int
"""
2018-04-20 12:17:27 +02:00
if self . player : self . player . set_seek ( position )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2018-04-20 12:17:27 +02:00
def set_volume ( self , volume ) :
2018-06-25 19:57:43 +02:00
"""
Set the volume
: param volume : Volume value between 0 and 100
: type volume : int
"""
2018-04-24 14:54:01 +02:00
# Transform a [0,100] value to an OMXPlayer volume in [-6000,0]
volume = 60.0 * volume - 6000
2018-04-20 12:17:27 +02:00
if self . player : self . player . set_volume ( volume )
return self . status ( )
2018-07-06 02:08:38 +02:00
@action
2017-12-27 10:18:51 +01:00
def status ( self ) :
2018-06-25 19:57:43 +02:00
"""
Get the current player state .
: returns : A dictionary containing the current state .
Example : :
output = {
" source " : " https://www.youtube.com/watch?v=7L9KkZoNZkA " ,
" state " : " play " ,
" volume " : 80 ,
" elapsed " : 123 ,
" duration " : 300 ,
" width " : 800 ,
" height " : 600
}
"""
2018-04-20 12:17:27 +02:00
state = PlayerState . STOP . value
2017-12-27 10:18:51 +01:00
if self . player :
2018-04-20 12:17:27 +02:00
state = self . player . playback_status ( ) . lower ( )
if state == ' playing ' : state = PlayerState . PLAY . value
elif state == ' stopped ' : state = PlayerState . STOP . value
elif state == ' paused ' : state = PlayerState . PAUSE . value
2018-07-06 02:08:38 +02:00
return {
2017-12-27 10:18:51 +01:00
' source ' : self . player . get_source ( ) ,
2018-04-20 12:17:27 +02:00
' state ' : state ,
2017-12-27 10:18:51 +01:00
' volume ' : self . player . volume ( ) ,
' elapsed ' : self . player . position ( ) ,
2018-04-20 12:17:27 +02:00
' duration ' : self . player . duration ( ) ,
' width ' : self . player . width ( ) ,
' height ' : self . player . height ( ) ,
2018-07-06 02:08:38 +02:00
}
2017-12-27 10:18:51 +01:00
else :
2018-07-06 02:08:38 +02:00
return {
2018-04-20 12:17:27 +02:00
' state ' : PlayerState . STOP . value
2018-07-06 02:08:38 +02:00
}
2017-12-27 10:18:51 +01:00
2018-07-06 02:08:38 +02:00
@action
2018-06-14 21:13:01 +02:00
def send_message ( self , msg ) :
try :
redis = get_backend ( ' redis ' )
if not redis :
raise KeyError ( )
except KeyError :
self . logger . warning ( " Backend {} does not implement send_message " +
" and the fallback Redis backend isn ' t configured " )
return
redis . send_message ( msg )
2018-04-24 14:36:05 +02:00
def on_play ( self ) :
def _f ( player ) :
2018-06-14 20:42:57 +02:00
self . send_message ( VideoPlayEvent ( video = self . player . get_source ( ) ) )
2018-04-24 14:36:05 +02:00
return _f
2018-04-19 22:42:28 +02:00
2018-04-24 14:36:05 +02:00
def on_pause ( self ) :
def _f ( player ) :
2018-06-14 20:42:57 +02:00
self . send_message ( VideoPauseEvent ( video = self . player . get_source ( ) ) )
2018-04-24 14:36:05 +02:00
return _f
2018-04-19 22:42:28 +02:00
2018-04-24 14:36:05 +02:00
def on_stop ( self ) :
def _f ( player ) :
2018-06-14 20:42:57 +02:00
self . send_message ( VideoStopEvent ( ) )
2018-04-24 14:36:05 +02:00
return _f
def _init_player_handlers ( self ) :
if not self . player :
return
self . player . playEvent + = self . on_play ( )
self . player . pauseEvent + = self . on_pause ( )
self . player . stopEvent + = self . on_stop ( )
2018-04-19 22:42:28 +02:00
2018-07-06 02:08:38 +02:00
@action
2018-04-25 00:13:17 +02:00
def search ( self , query , types = None , queue_results = False , autoplay = False ) :
2018-06-25 19:57:43 +02:00
"""
Perform a video search .
: param query : Query string , video name or partial name
: type query : str
: param types : Video types to search ( default : ` ` [ " youtube " , " file " , " torrent " ] ` ` )
: type types : list
: param queue_results : Append the results to the current playing queue ( default : False )
: type queue_results : bool
: param autoplay : Play the first result of the search ( default : False )
: type autoplay : bool
"""
2018-04-25 00:13:17 +02:00
results = [ ]
if types is None :
2018-04-25 11:29:03 +02:00
types = { ' youtube ' , ' file ' , ' torrent ' }
2018-04-20 09:38:04 +02:00
2018-04-25 00:13:17 +02:00
if ' file ' in types :
file_results = self . file_search ( query ) . output
results . extend ( file_results )
2018-04-20 09:38:04 +02:00
2018-04-25 11:29:03 +02:00
if ' torrent ' in types :
2018-10-22 18:26:11 +02:00
torrents = get_plugin ( ' torrent ' )
torrent_results = torrents . search ( query ) . output
2018-04-25 11:29:03 +02:00
results . extend ( torrent_results )
2018-04-25 00:13:17 +02:00
if ' youtube ' in types :
yt_results = self . youtube_search ( query ) . output
results . extend ( yt_results )
if results :
if queue_results :
self . videos_queue = [ _ [ ' url ' ] for _ in results ]
if autoplay :
self . play ( self . videos_queue . pop ( 0 ) )
elif autoplay :
self . play ( results [ 0 ] [ ' url ' ] )
2018-04-20 09:38:04 +02:00
2018-07-06 02:08:38 +02:00
return results
2018-04-19 10:18:46 +02:00
2018-04-25 11:29:03 +02:00
@classmethod
def _is_video_file ( cls , filename ) :
is_video = False
for ext in cls . video_extensions :
if filename . lower ( ) . endswith ( ext ) :
is_video = True
break
return is_video
2018-07-06 02:08:38 +02:00
@action
2018-04-24 21:28:42 +02:00
def file_search ( self , query ) :
results = [ ]
2018-04-25 00:13:17 +02:00
query_tokens = [ _ . lower ( ) for _ in re . split ( ' \ s+ ' , query . strip ( ) ) ]
2018-04-24 21:28:42 +02:00
for media_dir in self . media_dirs :
2018-06-06 20:09:18 +02:00
self . logger . info ( ' Scanning {} for " {} " ' . format ( media_dir , query ) )
2018-04-24 21:28:42 +02:00
for path , dirs , files in os . walk ( media_dir ) :
for f in files :
2018-04-25 11:29:03 +02:00
if not self . _is_video_file ( f ) :
2018-04-24 21:28:42 +02:00
continue
matches_query = True
for token in query_tokens :
if token not in f . lower ( ) :
matches_query = False
break
if not matches_query :
continue
2018-04-25 00:13:17 +02:00
results . append ( {
' url ' : ' file:// ' + path + os . sep + f ,
' title ' : f ,
} )
2018-04-24 21:28:42 +02:00
2018-07-06 02:08:38 +02:00
return results
2018-04-24 21:28:42 +02:00
2018-07-06 02:08:38 +02:00
@action
2018-04-19 10:18:46 +02:00
def youtube_search ( self , query ) :
2018-12-12 22:31:36 +01:00
"""
Performs a YouTube search either using the YouTube API ( faster and
recommended , it requires the : mod : ` platypush . plugins . google . youtube `
plugin to be configured ) or parsing the HTML search results ( fallback
slower method )
"""
2018-06-06 20:09:18 +02:00
self . logger . info ( ' Searching YouTube for " {} " ' . format ( query ) )
2018-04-25 00:13:17 +02:00
2018-12-12 22:31:36 +01:00
try :
2018-12-12 22:46:32 +01:00
return self . _youtube_search_api ( query = query )
2018-12-12 22:31:36 +01:00
except Exception as e :
self . logger . warning ( ' Unable to load the YouTube plugin, falling ' +
' back to HTML parse method: {} ' . format ( str ( e ) ) )
return self . _youtube_search_html_parse ( query = query )
2018-12-12 22:46:32 +01:00
def _youtube_search_api ( self , query ) :
return [
{
' url ' : ' https://www.youtube.com/watch?v= ' + item [ ' id ' ] [ ' videoId ' ] ,
2018-12-12 22:52:51 +01:00
' title ' : item . get ( ' snippet ' , { } ) . get ( ' title ' , ' <No Title> ' ) ,
2018-12-12 22:46:32 +01:00
}
for item in get_plugin ( ' google.youtube ' ) . search ( query = query ) . output
if item . get ( ' id ' , { } ) . get ( ' kind ' ) == ' youtube#video '
]
2018-12-12 22:31:36 +01:00
def _youtube_search_html_parse ( self , query ) :
2018-04-19 00:30:39 +02:00
query = urllib . parse . quote ( query )
url = " https://www.youtube.com/results?search_query= " + query
response = urllib . request . urlopen ( url )
2018-05-13 14:29:27 +02:00
html = response . read ( ) . decode ( ' utf-8 ' )
2018-04-19 10:18:46 +02:00
results = [ ]
2018-04-19 00:30:39 +02:00
2018-05-13 14:29:27 +02:00
while html :
m = re . search ( ' (<a href= " (/watch \ ?v=.+?) " .+?yt-uix-tile-link.+?title= " (.+?) " .+?>) ' , html )
2018-04-20 09:38:04 +02:00
if m :
2018-04-24 14:36:05 +02:00
results . append ( {
2018-05-13 14:29:27 +02:00
' url ' : ' https://www.youtube.com ' + m . group ( 2 ) ,
' title ' : m . group ( 3 )
2018-04-24 14:36:05 +02:00
} )
2018-04-19 10:18:46 +02:00
2018-05-13 14:29:27 +02:00
html = html . split ( m . group ( 1 ) ) [ 1 ]
else :
html = ' '
2018-06-06 20:09:18 +02:00
self . logger . info ( ' {} YouTube video results for the search query " {} " '
2018-04-20 09:30:19 +02:00
. format ( len ( results ) , query ) )
2018-07-06 02:08:38 +02:00
return results
2018-04-19 00:30:39 +02:00
2017-12-27 10:18:51 +01:00
@classmethod
def _get_youtube_content ( cls , url ) :
m = re . match ( ' youtube:video:(.*) ' , url )
if m : url = ' https://www.youtube.com/watch?v= {} ' . format ( m . group ( 1 ) )
proc = subprocess . Popen ( [ ' youtube-dl ' , ' -f ' , ' best ' , ' -g ' , url ] ,
stdout = subprocess . PIPE )
return proc . stdout . read ( ) . decode ( " utf-8 " , " strict " ) [ : - 1 ]
# vim:sw=4:ts=4:et: