5
# Copyright (C) 2012 Thomas Mashos
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
18
# You should have received a copy of the GNU General Public License
19
# along with this program. If not, see <http://www.gnu.org/licenses/>.
24
from gi.repository import (
36
from urlparse import urlparse
42
APP_NAME = "unity-lens-video"
43
LOCAL_PATH = "/usr/share/locale/"
46
gettext.bindtextdomain(APP_NAME, LOCAL_PATH)
47
gettext.textdomain(APP_NAME)
51
class XDGError(Exception): pass
52
class FileNotFoundError(Exception): pass
54
socket.setdefaulttimeout(5)
55
BUS_NAME = "net.launchpad.scope.mythtv"
59
self.mt = mythtvapi.MythTVService()
61
self.scope = Unity.Scope.new ("/net/launchpad/scope/mythtv")
62
self.scope.search_in_global = False
63
self.scope.connect("search-changed", self.on_search_changed)
64
#self.scope.connect("filters-changed", self.on_search_changed)
65
self.scope.connect("activate-uri", self.on_activate_uri)
66
self.scope.connect('preview-uri', self.on_preview_uri)
67
self.scope.connect("notify::active", self.on_lens_active)
68
self.scope.connect("filters-changed", self.on_filtering_changed)
69
self.scope.props.sources.connect("notify::filtering",
70
self.on_filtering_changed)
71
#self.scope.search_in_global = False
74
if os.path.isfile("/usr/bin/mythavtest"):
75
self.player = "/usr/bin/mythavtest"
76
self.scope.props.sources.add_option('MythTV Shows', 'MythTV Shows', None)
77
self.scope.props.sources.add_option('MythTV Movies', 'MythTV Movies', None)
78
self.sources_list = []
81
def on_filtering_changed(self, *_):
82
"""Run another search when a filter change is notified."""
83
self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
85
def on_lens_active(self, *_):
86
""" Run a search when the lens is opened """
87
if self.scope.props.active:
88
self.scope.queue_search_changed(Unity.SearchType.DEFAULT)
90
def on_activate_uri (self, scope, uri):
91
playuri = uri.split('lens-meta://')[0]
92
if not self.player == "/usr/bin/mythavtest":
93
p, VAR = self.get_defaults('video/mpeg')['exec'].split(' ', 1)
94
vidplayer = subprocess.Popen(['which', p],
95
stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
96
self.player = vidplayer[0].strip()
98
GLib.spawn_async([str(self.player), playuri])
99
return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH, goto_uri='')
101
def on_preview_uri (self, scope, rawuri):
102
"""Preview request handler"""
103
URI, TYPE, ID, STARTTIME, CHANID = rawuri.split('lens-meta://')
104
BACKEND = urlparse(URI).netloc
105
details = self.mt.getDetails(TYPE, ID, STARTTIME, CHANID)
109
if isinstance(details, dict):
110
title = details['Title']
111
subtitle = details['SubTitle']
112
thumbnail_uri = "http://%s/%s" % (BACKEND,details['Coverart'])
113
desc = details['Description']
114
source = details['ContentType']
115
if source == "TELEVISION":
116
subtitle += ", S%sE%s" % (details["Season"], details["Episode"])
118
thumbnail_file = Gio.File.new_for_uri (thumbnail_uri)
119
thumbnail_icon = Gio.FileIcon.new(thumbnail_file)
121
thumbnail_icon = None
126
if "Airdate" in details:
129
# Television airdate will be YYYY-MM-DD
130
release_date = datetime.datetime.strptime(details["Airdate"], "%Y-%m-%d")
132
print "Failed to parse release_date since it was not in format: YYYY-MM-DD"
133
elif "ReleaseDate" in details:
136
# MOVIE release date will be YYYY-MM-DDTHH:MM:SSZ
137
release_date = datetime.datetime.strptime(details["ReleaseDate"], "%Y-%m-%dT%H:%M:%SZ")
139
print "Failed to parse release_date since it was not in format: YYYY-MM-DDTHH:MM:SSZ"
140
if "Duration" in details:
141
# Given in seconds, convert to minutes
142
duration = details["Duration"]
144
#if release_date != None:
145
# subtitle = release_date.strftime("%Y")
147
preview = Unity.MoviePreview.new(title, subtitle, desc, thumbnail_icon)
148
play_video = Unity.PreviewAction.new("play", _("Play"), None)
149
play_video.connect('activated', self.play_video)
150
preview.add_action(play_video)
152
#blah_video = Unity.PreviewAction.new("blah", _("Blah"), None)
153
#blah_video.connect('activated', self.play_video)
154
#preview.add_action(blah_video)
156
# For now, rating == -1 and num_ratings == 0 hides the rating widget from the preview
157
rating = float(details['Stars'])
158
preview.set_rating(rating, 0)
161
d = "%d mins" % int(duration)
162
preview.add_info(Unity.InfoHint.new("Length", _("Length"),
165
if source == "MOVIE":
166
if "Tagline" in details:
167
tagline = details["Tagline"]
169
preview.add_info(Unity.InfoHint.new("tagline",
170
"Tagline", None, tagline))
171
if "Certification" in details:
172
Cert = details["Certification"]
174
preview.add_info(Unity.InfoHint.new("Rated",
175
"Rated", None, Cert))
176
if "Director" in details:
177
directors = details["Director"]
178
preview.add_info(Unity.InfoHint.new("Director",
179
n_("Director", "Directors", 1), None, directors))
180
if "Studio" in details:
181
studios = details["Studio"]
183
preview.add_info(Unity.InfoHint.new("studios",
184
"Studio", None, studios))
185
elif source == "TELEVISION":
186
if "Airdate" in details:
187
preview.add_info(Unity.InfoHint.new("Airdate", _("Airdate"),
188
None, details["Airdate"]))
189
if "ChannelName" in details:
190
preview.add_info(Unity.InfoHint.new("Channel", _("Channel"),
191
None, details["ChannelName"]))
195
def _get_app_paths():
196
paths = os.environ.get('XDG_DATA_HOME',
197
os.path.expanduser('~/.local/share/')).split(os.path.pathsep)
198
paths.extend(os.environ.get('XDG_DATA_DIRS',
199
'/usr/local/share/:/usr/share/').split(os.path.pathsep))
202
def xdg_query(self, command, parameter):
203
p = subprocess.Popen(['xdg-mime', 'query', command, parameter],
204
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
205
output, errors = p.communicate()
206
if p.returncode or errors:
207
raise XDGError('xdg-mime returned error code %d: %s' %
208
(p.returncode, errors.strip()))
209
return output.strip()
211
def play_video (self, action, uri):
212
"""Preview request - Play action handler"""
213
return self.on_activate_uri (action, uri)
215
def locate_desktop_file(self, filename, mode='r', encoding='utf-8', _paths=_get_app_paths()):
217
for thispath, dirs, files in os.walk(os.path.join(path, 'applications')):
218
if filename not in files:
220
fullname = os.path.join(thispath, filename)
222
return codecs.open(fullname, mode, encoding)
226
raise FileNotFoundError(filename)
228
def get_defaults(self, MIMETYPE):
229
desktop_filename = self.xdg_query('default', MIMETYPE)
230
with self.locate_desktop_file(desktop_filename) as desktop_file:
231
parser = ConfigParser.ConfigParser()
232
parser.readfp(desktop_file, desktop_filename)
233
return dict(parser.items(parser.sections()[0]))
235
def get_search_string (self):
236
search = self.scope.props.active_search
237
return search.props.search_string if search else None
239
def on_search_changed(self, scope, search, search_type, _):
240
"""Get the search string, prepare a list of activated sources for
241
the server, update the model and notify the lens when we are done."""
242
search_string = search.props.search_string.strip()
243
# note search_string is a utf-8 encoded string, now:
244
print "Search changed to %r" % search_string
245
model = search.props.results_model
247
# Create a list of activated sources
248
sources = [source for n, source in enumerate(self.sources_list)
249
if self.source_activated(str(n))]
250
#Ensure a MythTV source is selected
251
if self.source_activated('MythTV Shows') or self.source_activated('MythTV Movies'):
252
# Ensure that we are not in Global search
253
if search_type is Unity.SearchType.DEFAULT:
254
#if len(search_string) > 0:
255
self.update_results_model(search_string, model, sources)
256
model.flush_revision_queue()
260
def source_activated(self, source):
261
"""Return the state of a sources filter option."""
262
active = self.scope.props.sources.get_option(source).props.active
263
filtering = self.scope.props.sources.props.filtering
264
if (active and filtering) or (not active and not filtering):
269
def update_results_model(self, search, model, sources):
270
for result in self.mt.search(SEARCHTERM = search):
272
#if self.player == "/usr/bin/mythavtest":
273
# BACKEND = urlparse(result['BackendLocation']).netloc.split(':')[0]
274
# URI = 'myth://'+result['StorageGroup']+'@'+BACKEND+':'+result['MythProtocolPort']+'/'+result['FileName']
275
# DNDURI = result['BackendLocation']+"/Content/GetFile?StorageGroup="+result['StorageGroup']+"&FileName="+result['FileName']
277
#URI = result['BackendLocation']+"/Content/GetFile?StorageGroup="+result['StorageGroup']+"&FileName="+result['FileName']
280
URI = "%s/Content/GetFile?StorageGroup=%s&FileName=%slens-meta://%slens-meta://%slens-meta://%slens-meta://%s" % (result['BackendLocation'], result['StorageGroup'], result['FileName'], result['ContentType'], result['Id'], result['StartTime'], result['ChanId'])
282
DNDURI = result['BackendLocation']+"/Content/GetFile?StorageGroup="+result['StorageGroup']+"&FileName="+result['FileName']
284
mimetype = mimetypes.guess_type(result['FileName'])[0]
286
icon = result['BackendLocation']+result['Coverart']
288
icon = Gio.ThemedIcon.new ("video").to_string()
289
## Fix the Name and 0nly add the result if the correct source is activated
290
if self.source_activated('MythTV Shows') and result['ContentType'] == 'TELEVISION':
291
NAME = result['Title']+' - S'+result['Season']+'E'+result['Episode']+' - '+result['SubTitle']
293
elif self.source_activated('MythTV Movies') and result['ContentType'] == 'MOVIE':
294
NAME = result['Title']+' '+result['SubTitle']
296
if result['Description'] == None:
299
DESCRIPTION = result['Description']
309
DESCRIPTION, #comment
312
model.flush_revision_queue ()
313
#if self.scope.props.active_search:
314
# self.scope.props.active_search.emit('finished')
316
if __name__ == "__main__":
317
session_bus_connection = Gio.bus_get_sync (Gio.BusType.SESSION, None)
318
session_bus = Gio.DBusProxy.new_sync (session_bus_connection, 0, None,
319
'org.freedesktop.DBus',
320
'/org/freedesktop/DBus',
321
'org.freedesktop.DBus', None)
322
result = session_bus.call_sync('RequestName',
323
GLib.Variant ("(su)", (BUS_NAME, 0x4)),
326
# Unpack variant response with signature "(u)". 1 means we got it.
327
result = result.unpack()[0]
330
print >> sys.stderr, "Failed to own name %s. Unity-Scope-MythTV is working." % BUS_NAME
334
GObject.MainLoop().run()