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)
|