~ubuntutv-dev-team/ubuntutv/unity-lens-video

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#
# IDEAS AND COMMENTS
#
# - We should probably cache all Video instances on startup to avoid recreating
#   them on each search. Listen to the "changed" signal on the GMenu tree to
#   update
#
# - Matching and scoring algorithm is really lame. Could be more efficient,
#   and calculate the scores better
#
# - Bear in mind when looking at this code that this is *meant* to be simple :-)
#

import sys, os, re
from gi.repository import GLib, GObject, Gio
from gi.repository import Dee
from gi.repository import GMenu
from gi.repository import Unity
from os import path, listdir
import xml.etree.ElementTree as ET

# These category ids must match the order in which we add them to the lens
CATEGORY_FEATURED = 0
CATEGORY_RENTED = 1
CATEGORY_PURCHASED = 2
CATEGORY_RECORDED = 3

GENRE_MAP = {
    "action_adventure": ("Action", "Adventure", "Action & Adventure", "Action and Adventure", "Crime"),
    "animation": ("Animation",),
    "arthouse_cult": ("Arthouse", "Cult"),
    "comedy": ("Comedy",),
    "classics": ("Classics",),
    "documentary": ("Documentary",),
    "drama": ("Drama",),
    "family_fantasy": ("Children", "Family", "Fantasy", "Kids", "Sci-Fi & Fantasy"),
    "horror": ("Horror",),
    "music_musical": ("Music", "Musical"),
    "mystery_thriller": ("Disaster", "Film-Noir", "Film Noir", "Mystery", "Suspense", "Thriller"),
    "news": ("News",),
    "romance": ("Romance",),
    "sci-fi": ("Science-Fiction", "Science Fiction", "Sci-Fi", "Sci-Fi & Fantasy"),
    "special": ("History", "Holiday", "Home and Garden", "Special Interest"),
    "sport": ("Sport", "Sporting Event", "Sports Film"),
    "tv": ("Game-Show", "Game Show", "Reality", "Reality-TV", "Soap", "Talk", "Talk-Show", "Talk Show"),
    "western": ("Western",),
    # TODO: Biography, British, Crime?, Eastern, Education, Erotic, Fan Film,
    # Film-Noir?, Film Noir?, Foreign, Game-Show?, Game Show?, History?, Holiday?
    # Home and Garden?, Indie, Mini-Series, Neo-Noir, Reality?, Reality-TV?, Road Movie
    # Short, Soap?, War?
}

SPLIT_RE = re.compile('[\s,\.\-]')

class Video:
    """Simple representation of an item in the result model.
       This class implements simple comparisons, sorting, and hashing
       methods to integrate with the Python routines for these things
    """
    def __init__ (self, uri, icon, genres, mime, display_name, comment,
                  dnd_uri, score=0, mtime=0):
        self.uri = uri
        self.icon = icon
        self.genres = genres
        self.mime = mime
        self.display_name = display_name
        self.comment = comment
        self.dnd_uri = dnd_uri
        self.score = score
        self.mtime = mtime
    
    def __lt__ (self, other):
        if self.mtime and self.mtime != other.mtime:
            return self.mtime > other.mtime # reverse modification time
        return self.display_name < other.display_name
    
    def __gt__ (self, other):
        if other.mtime and other.mtime != self.mtime:
            return self.mtime < other.mtime # reverse modification time
        return self.display_name < other.display_name
    
    def __eq__ (self, other):
        return self.uri == other.uri
    
    def __ne__ (self, other):
        return self.uri != other.uri
        
    def __hash__ (self):
        return hash(self.uri)


class Scope (Unity.Scope):
    filter_genre = None
    filter_sorting = None
    filter_source = None

    def __init__ (self):
        Unity.Scope.__init__ (self, dbus_path="/com/canonical/unity/lens/video")
        
        # Listen for changes and requests
        self.connect ("notify::active-search", self._on_search_changed)
        self.connect ("notify::active-global-search", self._on_global_search_changed)
        self.connect ("activate-uri", self.activate_uri)
        self.connect ("filters-changed", self._on_filters_changed)
            
        self.video_root_dir = path.expanduser("~/Videos/unity/local")
    
    def get_search_string (self):
        search = self.props.active_search
        return search.props.search_string if search else None
    
    def get_global_search_string (self):
        search = self.props.active_global_search
        return search.props.search_string if search else None
    
    def search_finished (self):
        search = self.props.active_search
        if search:
            search.emit("finished")
    
    def global_search_finished (self):
        search = self.props.active_global_search
        if search:
            search.emit("finished")
    
    def activate_uri (self, scope, uri):
        """Activation handler to enable browsing of app directories"""
        
        print "Activate: %s" % uri
        
        return Unity.ActivationResponse.new (Unity.HandledType.NOT_HANDLED, uri)

        # TODO: take care of activation here. Ideally by simply telling to the
        # unity-2d-shell player via DBUS to start the requested video.
        #return Unity.ActivationResponse.new (Unity.HandledType.SHOW_DASH, uri)
            
    def _on_search_changed (self, scope, param_spec):
        search = self.get_search_string()
        results = scope.props.results_model
        
        print "Search changed to: '%s'" % search
        
        self._do_search (search, results)
        self.search_finished()
    
    def _on_global_search_changed (self, scope, param_spec):
        search = self.get_global_search_string()
        results = scope.props.global_results_model
        
        print "Global search changed to: '%s'" % search
        
        self._do_search(search, results)
        self.global_search_finished()

    def _on_filters_changed(self, scope):
        self.filter_genre = self.get_filter('genres').get_active_option()
        self.filter_sorting = self.get_filter('sorting').get_active_option()
        self.filter_source = self.get_filter('sources').get_active_option()
        
        search = self.get_search_string()
        results = scope.props.results_model
        
        self._do_search(search, results)
        self.search_finished()

    def _collect_videos (self, videopath, collector):
        for filename in listdir(videopath):
            fullpath = path.join(videopath, filename)
            # iterate recursively in all subdirs (for series, seasons, movies inside dirs)
            if path.isdir(fullpath):
                collector = self._collect_videos(fullpath, collector)
                continue

            if not path.isfile(fullpath):
                continue

            # Drop anything that's not a video file
            mimetype, _ = Gio.content_type_guess(fullpath, None)
            if not mimetype.startswith("video/"):
                continue

            filename, _ = path.splitext(fullpath)
            _, title = path.split(filename)
            rating = 0.0
            genres = set()

            # If xmbc metadata is available, try to parse it to prettify the results
            infofile = filename + ".nfo"
            if path.isfile(infofile):

                # Get movie/episode title
                tree = ET.parse(infofile)
                title = tree.findtext("title", title)
                rating = float(tree.findtext("rating", rating)) * 0.5
                genres = {genre.text for genre in tree.findall('genre')}

                # Check if it's an episode, in which case retrieve series/ep info and the
                # show name (look for it in the current folder or in the one above to
                # cover for Show/Series or just ShowSeries/ directory organizations)
                showfile = path.join(videopath, "tvshow.nfo")
                if not path.isfile(showfile):
                    updir, _ = path.split(videopath)
                    showfile = path.join(updir, "tvshow.nfo")
                if path.isfile(showfile):
                    season = tree.findtext("season", -1)
                    episode = tree.findtext("episode", -1)
                    if season != -1 and episode != -1:
                        title = "%dx%02d %s" % (int(season), int(episode), title)

                    tree = ET.parse(showfile)
                    showname = tree.findtext("title", "Unknown Show")
                    title = "%s %s" % (showname, title)
                    genres = {genre.text for genre in tree.findall('genre')}

            # If we have a poster file or some artwork use it as icon, otherwise just use
            # the generic stock videofile icon.
            posterfile = filename + ".tbn"
            if not path.isfile(posterfile):
                icon = Gio.ThemedIcon.new ("video").to_string()
            else:
                icon = posterfile

            collector.add(Video (
	                           "file://" + fullpath, # uri
	                           icon, # string formatted GIcon
	                           genres, # genres
	                           mimetype, # mimetype
	                           title, # display name
	                           fullpath, # comment
	                           fullpath, # dnd uri
	                           rating, # score
	                           os.stat(fullpath).st_mtime))
        return collector

    def _do_search (self, search_string, model):
        model.clear ()
        
        if search_string:
            search_parts = set(SPLIT_RE.split(search_string.lower()))
        else:
            search_parts = None

        def search(video):
            data = " ".join((video.uri, video.mime, video.display_name, video.comment) + tuple(video.genres)).lower()
            for part in search_parts:
                if data.find(part) == -1:
                    return False
            return True

        if search_parts:
            featured = set(filter(search, self._collect_videos(path.join(self.video_root_dir, "featured"), set())))
        else:
            featured = self._collect_videos(path.join(self.video_root_dir, "featured"), set())

        if self.filter_genre and self.filter_genre.props.id != "all":
            featured = {movie for movie in featured if movie.genres.intersection(GENRE_MAP[self.filter_genre.props.id])}
        # TODO implement filter_sources 
        
        if self.filter_sorting and self.filter_sorting.props.id != "recent":
            if self.filter_sorting.props.id == "popular":
                result = sorted (featured, key=lambda x: -x.score)
            if self.filter_sorting.props.id == "alphabetical":
                result = sorted (featured, key=lambda x: x.display_name.lower())
        else:
            result = sorted (featured)
        for video in result:
            model.append (video.uri,
                          video.icon,
                          CATEGORY_FEATURED,
                          video.mime,
                          video.display_name,
                          video.comment,
                          video.dnd_uri)

        if search_parts:
            rented = set(filter(search, self._collect_videos(path.join(self.video_root_dir, "rented"), set())))
        else:
            rented = self._collect_videos(path.join(self.video_root_dir, "rented"), set())

        if self.filter_genre and self.filter_genre.props.id != "all":
            rented = {movie for movie in rented if movie.genres.intersection(GENRE_MAP[self.filter_genre.props.id])}
        # TODO implement filter_sources 
        
        if self.filter_sorting and self.filter_sorting.props.id != "recent":
            if self.filter_sorting.props.id == "popular":
                result = sorted (rented, key=lambda x: -x.score)
            if self.filter_sorting.props.id == "alphabetical":
                result = sorted (rented, key=lambda x: x.display_name.lower())
        else:
            result = sorted (rented)
        for video in result:
            model.append (video.uri,
                          video.icon,
                          CATEGORY_RENTED,
                          video.mime,
                          video.display_name,
                          video.comment,
                          video.dnd_uri)

        if search_parts:
            purchased = set(filter(search, self._collect_videos(path.join(self.video_root_dir, "purchased"), set())))
        else:
            purchased = self._collect_videos(path.join(self.video_root_dir, "purchased"), set())

        if self.filter_genre and self.filter_genre.props.id != "all":
            purchased = {show for show in purchased if show.genres.intersection(GENRE_MAP[self.filter_genre.props.id])}
        # TODO implement filter_sources 
        
        if self.filter_sorting and self.filter_sorting.props.id != "recent":
            if self.filter_sorting.props.id == "popular":
                result = sorted (purchased, key=lambda x: -x.score)
            if self.filter_sorting.props.id == "alphabetical":
                result = sorted (purchased, key=lambda x: x.display_name.lower())
        else:
            result = sorted (purchased)
        for video in result:
            model.append (video.uri,
                          video.icon,
                          CATEGORY_PURCHASED,
                          video.mime,
                          video.display_name,
                          video.comment,
                          video.dnd_uri)
            
        if search_parts:
            recorded = set(filter(search, self._collect_videos(path.join(self.video_root_dir, "recorded"), set())))
        else:
            recorded = self._collect_videos(path.join(self.video_root_dir, "recorded"), set())

        if self.filter_genre and self.filter_genre.props.id != "all":
            recorded = {show for show in recorded if show.genres.intersection(GENRE_MAP[self.filter_genre.props.id])}
        # TODO implement filter_sources 
        
        if self.filter_sorting and self.filter_sorting.props.id != "recent":
            if self.filter_sorting.props.id == "popular":
                result = sorted (recorded, key=lambda x: -x.score)
            if self.filter_sorting.props.id == "alphabetical":
                result = sorted (recorded, key=lambda x: x.display_name.lower())
        else:
            result = sorted (recorded)
        for video in result:
            model.append (video.uri,
                          video.icon,
                          CATEGORY_RECORDED,
                          video.mime,
                          video.display_name,
                          video.comment,
                          video.dnd_uri)