~josephjamesmills/beta/unity-scope-mythtv

« back to all changes in this revision

Viewing changes to src/unity-scope-mythtv

  • Committer: Thomas Mashos
  • Date: 2012-10-19 21:17:40 UTC
  • Revision ID: thomas@mashos.com-20121019211740-fhjwyo4me40z5a2b
Added setup.py file for installation. Added test scope for testing (from remote-videos scope)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/python
 
2
#
 
3
# Unity Scope MythTV
 
4
#
 
5
# Copyright (C) 2012 Thomas Mashos
 
6
# thomas@mashos.com
 
7
#
 
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.
 
12
#
 
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.
 
17
#
 
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/>.
 
20
#
 
21
#
 
22
#
 
23
import sys
 
24
from gi.repository import (
 
25
    GLib,
 
26
    GObject,
 
27
    Gio,
 
28
    Unity,
 
29
    Soup,
 
30
    SoupGNOME,
 
31
)
 
32
 
 
33
import mythtvapi
 
34
import socket
 
35
import os
 
36
from urlparse import urlparse
 
37
import subprocess
 
38
import codecs
 
39
import ConfigParser
 
40
import mimetypes
 
41
 
 
42
APP_NAME = "unity-lens-video"
 
43
LOCAL_PATH = "/usr/share/locale/"
 
44
 
 
45
import gettext
 
46
gettext.bindtextdomain(APP_NAME, LOCAL_PATH)
 
47
gettext.textdomain(APP_NAME)
 
48
_ = gettext.gettext
 
49
n_ = gettext.ngettext
 
50
 
 
51
class XDGError(Exception): pass
 
52
class FileNotFoundError(Exception): pass
 
53
 
 
54
socket.setdefaulttimeout(5)
 
55
BUS_NAME = "net.launchpad.scope.mythtv"
 
56
 
 
57
class Daemon:
 
58
    def __init__ (self):
 
59
        self.mt = mythtvapi.MythTVService()
 
60
        
 
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
 
72
        
 
73
        self.player = ''
 
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 = []
 
79
        self.scope.export()
 
80
 
 
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)
 
84
 
 
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)
 
89
 
 
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()
 
97
        #print uri
 
98
        GLib.spawn_async([str(self.player), playuri])
 
99
        return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH, goto_uri='')
 
100
 
 
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)
 
106
        subtitle = ''
 
107
        desc = ''
 
108
        source = ''
 
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"])
 
117
        try:
 
118
            thumbnail_file = Gio.File.new_for_uri (thumbnail_uri)
 
119
            thumbnail_icon = Gio.FileIcon.new(thumbnail_file)
 
120
        except:
 
121
            thumbnail_icon = None
 
122
        
 
123
        release_date = None
 
124
        duration = 0
 
125
            
 
126
        if "Airdate" in details:
 
127
            try:
 
128
                import datetime
 
129
                # Television airdate will be YYYY-MM-DD
 
130
                release_date = datetime.datetime.strptime(details["Airdate"], "%Y-%m-%d")
 
131
            except ValueError:
 
132
                print "Failed to parse release_date since it was not in format: YYYY-MM-DD"
 
133
        elif "ReleaseDate" in details:
 
134
            try:
 
135
                import datetime
 
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")
 
138
            except ValueError:
 
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"]
 
143
        
 
144
        #if release_date != None:
 
145
        #    subtitle = release_date.strftime("%Y")
 
146
        
 
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)
 
151
        
 
152
        #blah_video = Unity.PreviewAction.new("blah", _("Blah"), None)
 
153
        #blah_video.connect('activated', self.play_video)
 
154
        #preview.add_action(blah_video)
 
155
        
 
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)
 
159
        
 
160
        if duration > 0:
 
161
            d = "%d mins" % int(duration)
 
162
            preview.add_info(Unity.InfoHint.new("Length", _("Length"),
 
163
                    None, d))
 
164
        
 
165
        if source == "MOVIE":
 
166
            if "Tagline" in details:
 
167
                tagline = details["Tagline"]
 
168
                if tagline:
 
169
                    preview.add_info(Unity.InfoHint.new("tagline",
 
170
                        "Tagline", None, tagline))
 
171
            if "Certification" in details:
 
172
                Cert = details["Certification"]
 
173
                if Cert:
 
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"] 
 
182
                if studios:
 
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"]))
 
192
 
 
193
        return preview
 
194
 
 
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))
 
200
        return paths
 
201
 
 
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()
 
210
 
 
211
    def play_video (self, action, uri):
 
212
        """Preview request - Play action handler"""
 
213
        return self.on_activate_uri (action, uri)
 
214
 
 
215
    def locate_desktop_file(self, filename, mode='r', encoding='utf-8', _paths=_get_app_paths()):
 
216
        for path in _paths:
 
217
            for thispath, dirs, files in os.walk(os.path.join(path, 'applications')):
 
218
                if filename not in files:
 
219
                    continue
 
220
                fullname = os.path.join(thispath, filename)
 
221
                try:
 
222
                    return codecs.open(fullname, mode, encoding)
 
223
                except IOError:
 
224
                    pass
 
225
        else:
 
226
            raise FileNotFoundError(filename)
 
227
 
 
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]))
 
234
 
 
235
    def get_search_string (self):
 
236
        search = self.scope.props.active_search
 
237
        return search.props.search_string if search else None
 
238
 
 
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
 
246
        model.clear()
 
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()
 
257
            if search:
 
258
                search.finished()
 
259
 
 
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):
 
265
            return True
 
266
        else:
 
267
            return False
 
268
 
 
269
    def update_results_model(self, search, model, sources):
 
270
        for result in self.mt.search(SEARCHTERM = search):
 
271
            ADDRESULT=False
 
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']
 
276
            #else:
 
277
            #URI = result['BackendLocation']+"/Content/GetFile?StorageGroup="+result['StorageGroup']+"&FileName="+result['FileName']
 
278
#            URI = "BOBO"
 
279
            
 
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'])
 
281
 
 
282
            DNDURI = result['BackendLocation']+"/Content/GetFile?StorageGroup="+result['StorageGroup']+"&FileName="+result['FileName']
 
283
            
 
284
            mimetype = mimetypes.guess_type(result['FileName'])[0]
 
285
            try:
 
286
                icon = result['BackendLocation']+result['Coverart']
 
287
            except:
 
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']
 
292
                ADDRESULT=True
 
293
            elif self.source_activated('MythTV Movies') and result['ContentType'] == 'MOVIE':
 
294
                NAME = result['Title']+' '+result['SubTitle']
 
295
                ADDRESULT=True
 
296
            if result['Description'] == None:
 
297
                DESCRIPTION = ''
 
298
            else:
 
299
                DESCRIPTION = result['Description']
 
300
            if ADDRESULT:
 
301
                if mimetype == None:
 
302
                    mimetype = "video"
 
303
                model.append(
 
304
                    URI, #uri
 
305
                    icon, #icon
 
306
                    3, #category
 
307
                    mimetype, #mimetype
 
308
                    NAME, #display_name
 
309
                    DESCRIPTION, #comment
 
310
                    DNDURI) #dnd_uri
 
311
            
 
312
        model.flush_revision_queue ()
 
313
        #if self.scope.props.active_search:
 
314
        #    self.scope.props.active_search.emit('finished')
 
315
 
 
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)),
 
324
                                    0, -1, None)
 
325
                                
 
326
    # Unpack variant response with signature "(u)". 1 means we got it.
 
327
    result = result.unpack()[0]
 
328
    
 
329
    if result != 1 :
 
330
        print >> sys.stderr, "Failed to own name %s. Unity-Scope-MythTV is working." % BUS_NAME
 
331
        raise SystemExit (1)
 
332
    
 
333
    daemon = Daemon()
 
334
    GObject.MainLoop().run()